Information
- OpenAPI version:
3.1.0
Sotirea’s API lets your own software see and move the money your organisation holds in Sotirea.
Start in Test, where the networks are test networks and nothing is worth anything. In the Sotirea console:
Then, from your server, with the key in SOTIREA_API_KEY and the API’s address in SOTIREA_API:
curl -H "Authorization: Bearer $SOTIREA_API_KEY" $SOTIREA_API/v1/me
curl -H "Authorization: Bearer $SOTIREA_API_KEY" $SOTIREA_API/v1/vaults
/v1/me names the key and says test. Each vault lists the address to pay it on each network: pay test coin to it, and watch /v1/deposits show it arriving, then received and in /v1/balances once final. Then pay some of it out, naming the payment with your own id:
curl -X POST $SOTIREA_API/v1/payments \
-H "Authorization: Bearer $SOTIREA_API_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: order-1001" \
-d '{"vault_id": 1, "to": "0x...", "amount": "1000000000000000"}'
Follow it with GET /v1/payments/{id}, using the id the answer gave (api:<organisation id>:order-1001), or add a webhook (below) and be told. When it works in Test, a Live key runs the same code against real money.
Sotirea’s Go and TypeScript clients are written from the same types as this description, so they always match it. Each has a method per operation, named for what it does (GetMe, ListVaults, SendPayment, and so on), answers refusals as an error carrying code, rule and request_id, and verifies webhooks. Install them with go get sotirea.com/sdk/go (package sotirea) and npm install @sotirea/sdk.
// Go
client := sotirea.New(os.Getenv("SOTIREA_API"), os.Getenv("SOTIREA_API_KEY"))
payment, meta, err := client.SendPayment(ctx, "order-1001", sotirea.PaymentRequest{VaultID: 1, To: "0x...", Amount: "1000000000000000"})
// TypeScript
const sotirea = new Sotirea({ baseUrl: process.env.SOTIREA_API, apiKey: process.env.SOTIREA_API_KEY })
const payment = await sotirea.sendPayment('order-1001', { vault_id: 1, to: '0x...', amount: '1000000000000000' })
Every request carries an API key as a bearer token:
Authorization: Bearer sot_test_...
Keys are created in the Sotirea console, under Developers and API keys, by someone who manages roles. A key holds roles, as a person does, and can do what they allow and nothing more. A key is shown once, when it is created or rolled; Sotirea keeps only a hash of it. Keep it on your server: it is not for a browser or a mobile app.
For a key that runs an integration, give it the Integration role: it opens vaults, requests payments and manages webhooks, and sees balances, payments, gas pools and payment rules. Because it can move money, giving it to a key waits until a second person who manages roles confirms it, unless nobody else in your organisation could. A key never manages people or roles and never approves payments.
A key can be limited to the addresses your servers call from, on its page in the console. A request from any other address is answered 403 with fields.rule api_key_address, saying the key is limited to certain addresses.
A key starting sot_test_ reaches your Test organisation, where every network is a test network and nothing is worth anything. A key starting sot_live_ reaches your Live organisation and real money. Everything works the same in both, so an integration built against Test runs against Live with only the key changed. GET /v1/me says which one a key reaches.
Every answer has the same shape. Success:
{"success": true, "data": ..., "meta": {"request_id": "..."}}
Lists are paged with page and page_size, and meta.page says where you are: page, page_size, total, total_pages, has_next.
A refusal:
{"success": false, "error": {"code": "forbidden", "message": "...", "fields": {"rule": "balances.view"}}}
code is one of invalid, unauthenticated, forbidden, not_found, conflict, rate_limited, unavailable, internal, with the HTTP status to match. message is written for a person and may be shown to one. fields.rule, when present, names what refused the request, so your code can tell refusals apart without reading the words: the permission a key lacks (payments.request), the payment rule a payment broke, sanctioned_address or blocked_destination for an address Sotirea will not pay (see Sanctions screening), or idempotency_key for a key already used for a different payment. For invalid, fields names each field that was wrong instead.
Every answer, a refusal too, carries meta.request_id, also in the X-Request-Id header: quote it when asking about a request. Every field is always present: a value that may not exist, like a payment’s tx_hash before it is signed, is null, never missing or empty. Times are RFC 3339, in UTC.
POST /v1/payments with an Idempotency-Key header that names the payment, such as your own order id. If the answer is lost, send the same request with the same key: you get the same payment back, answered 202 again with its id and created_at unchanged, never a second one. Addresses are compared without regard to case. The same key with a different vault, amount, address, asset, network or reference is refused 409, with fields.rule idempotency_key. The payment’s id is api:<your organisation's id>:<the Idempotency-Key>; Idempotency-Keys are your organisation’s own, shared by all its API keys.
The answer comes at once, 202, while the payment goes on: requested, then signed and sent, and confirmed once the network has made it final. A payment your rules hold for approval is awaiting_approval until people in your organisation agree in the console, and its held_by names the rule holding it, like approval_threshold; a key never approves, and neither does anyone who created the key or gave it a new secret. A vault’s first payment on a network is awaiting_gas while the vault’s account is created there, and then goes on by itself. Every payment’s network fee is paid from your gas pool on that network, never from the amount: when the pool cannot pay it, the payment fails and the request is answered 400 with fields.rule insufficient_gas. Sending again with the same Idempotency-Key answers that failed payment, so top the pool up and send with a new key. Poll GET /v1/payments/{id} to follow it.
In Live, a new address waits. A Live organisation’s payment rules start with a 24-hour wait: a payment to an address your organisation has never paid is refused 403 with fields.rule new_destination until someone adds the address to your approved addresses in the console, and then destination_waiting until 24 hours after that. Payment rules can change the wait, from none to 30 days. Test starts with no wait.
Every address you pay is checked against OFAC’s sanctions list and Sotirea’s own blocked list, before any of your payment rules, and no rule switches it off. A payment to a listed address is refused 403 with fields.rule sanctioned_address (a sanctions list names it) or blocked_destination (Sotirea’s own list); nothing is sent or set aside. A quote for one answers outcome refused with the same rule, and adding one to your approved addresses is refused 400 with the same fields.rule. A list can name an address while a payment to it waits for approval: that payment is then failed, with payment.failed, and its money is released. GET /v1/addresses/{address} answers listed true for one, so you can say so before anyone tries. Which list entry named it is never shown.
A deposit from a listed address is credited, because the network has made it, and held for a compliance review: the balance’s held says how much, counted in available and never in spendable, and its deposit.received event has held true. Sotirea will contact you. Paying out of held money is refused 400 with fields.rule insufficient_balance, and the message says how much is held. Only that deposit is held, never the rest of the vault.
Instead of polling, your organisation can name addresses of its own, in the console under Developers and Webhooks, and Sotirea posts each event you choose to them as it happens:
payment.awaiting_approval, payment.sent, payment.confirmed, payment.failed, payment.stuck, deposit.arriving, deposit.received, gas.low.
Each is a POST of JSON:
{"id": "...", "type": "payment.sent", "created_at": "...", "api_version": "1", "environment": "test",
"data": {"object": "payment", "id": "api:12:order-42", "status": "sent", "vault_id": 1, "chain_id": "eip155:1",
"to": "0x...", "asset": "native", "amount": "1500000000000000000", "tx_hash": "0x..."}}
data carries what the event is about and the state it reached, nothing that changes later; fetch the payment for anything else. A payment’s purpose is payment, or sweep for one your sweeps sent between your own vaults; its tx_hash is null while it waits for approval; one sped up or cancelled is payment.sent again, with the new transaction’s hash. A deposit’s data has object deposit, state arriving or received, sweep true when one of your sweeps moved it, held true when it is held for a compliance review, and its tx_hash, from, to, asset and amount; a low gas pool’s has its network and address.
Webhooks are signed to the Standard Webhooks specification, so you can verify them with its library for your language and the endpoint’s secret (whsec_..., shown once when you add it). The headers are webhook-id (the event’s id, the same on every retry), webhook-timestamp (seconds) and webhook-signature: v1, and the base64 HMAC-SHA256, under the base64-decoded secret, of <webhook-id>.<webhook-timestamp>.<body>. Refuse a webhook whose signature does not match or whose timestamp is more than five minutes from now.
Answer with any 2xx within 10 seconds. Anything else, or no answer, is tried again: after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, and then every 10 hours, for about two days. A delivery that never succeeds is listed in the console, from where it can be sent again. A webhook whose every attempt has failed for 3 days is turned off: its people are emailed, nothing more is sent to it, and one action in the console turns it back on.
A webhook can be given a new secret in the console, with the old one stopping at once or kept working for an hour, a day or a week. Until then each webhook carries a signature from each secret, space separated in webhook-signature, so a receiver holding either verifies it; the Standard Webhooks libraries, and Sotirea’s clients, already accept any one that matches. An event can arrive more than once and out of order: use webhook-id to ignore a repeat, and the payment’s or deposit’s own status rather than the order events arrived in.
Amounts are strings holding whole numbers of an asset’s smallest unit, never decimals: 1.5 of an asset with 18 decimals is "1500000000000000000". decimals says how to scale one for a person. It is null for a token whose details have not been read yet.
Each key may make about 25 reads and 5 changes a second, with bursts of twice that; each address 100 requests a second. Every answer says where a key stands in RateLimit-Limit (the burst: 50 reads or 10 changes), RateLimit-Remaining and RateLimit-Reset (seconds until full). A refused request is answered 429 with Retry-After in seconds.
GET /v1/statement?from=2026-01-01&to=2026-01-31 answers a statement as CSV: for each asset, the opening balance, every movement made final in the period and the closing balance, which opening plus the movements always equals. Add &vault= for one vault’s. Amounts are exact decimals beside their base units, times are UTC, and a token not on Sotirea’s recognised list is marked recognised false, whatever symbol it claims. The key needs balances.view.
/status says, for anybody and with no key, whether each part of Sotirea is working: the API, and for Test and Live, the networks it follows and the signing of payments. /v1/status is the same as JSON, each part operational, delayed, disrupted, not_open or unknown. It is checked at most every 30 seconds.
This is version 1, in every path as /v1. Within it, changes only add: new fields, new routes, new values of a status. Read only the fields you need and treat a value you do not recognise as one you do not handle yet. A change that would break an integration comes as /v2, served beside v1.
Your API key, created in the console under Developers and API keys. A Test key reaches your Test organisation, a Live key your Live one.
Security scheme type: http
Bearer format: sot_test_... or sot_live_...