Handle refusals
When Sotirea refuses a request, it says exactly why: code is the kind of refusal, fields.rule
names what refused it, and message explains it in words you can show a person. Your code should
decide on the rule, never on the words, which can change.
Retry, or change something
Section titled “Retry, or change something”rate_limited(429),unavailable(503) andinternal(500) are about the moment, not the request. Send the same request again after a short wait, with the same Idempotency-Key for a payment.- Everything else needs something to change first: the request, the key, an approval, or your organisation’s rules. Sending it again unchanged is refused again.
- No answer at all, from a timeout or a dropped connection, is not a refusal. For a payment, send it again with the same Idempotency-Key: if it was made, you get it back.
The payout example retries the first kind and returns the rest.
Decide by the rule
Section titled “Decide by the rule”// explain turns a refusal into what your team should do about it.func explain(err error) string { var refusal *sotirea.Error if !errors.As(err, &refusal) { return "no answer from Sotirea: send it again with the same payout id" } switch refusal.Rule() { case "new_destination", "allowlist": return "approve this address in the console before paying it" case "destination_waiting": return "this address was just approved: " + refusal.Message case "insufficient_balance": return "the vault cannot cover it: " + refusal.Message case "insufficient_gas": return "top up the network fees for Base, then pay with a new payout id" case "sanctioned_address", "blocked_destination": return "never pay this address" case "idempotency_key": return "this payout id was already used for a different payment" } // Any other refusal: its message is written to be shown to a person. return fmt.Sprintf("%s (request %s)", refusal.Message, refusal.RequestID)}// explain turns a refusal into what your team should do about it.export function explain(err: unknown): string { if (!(err instanceof SotireaError)) { return 'no answer from Sotirea: send it again with the same payout id' } switch (err.rule) { case 'new_destination': case 'allowlist': return 'approve this address in the console before paying it' case 'destination_waiting': return `this address was just approved: ${err.message}` case 'insufficient_balance': return `the vault cannot cover it: ${err.message}` case 'insufficient_gas': return 'top up the network fees for Base, then pay with a new payout id' case 'sanctioned_address': case 'blocked_destination': return 'never pay this address' case 'idempotency_key': return 'this payout id was already used for a different payment' } // Any other refusal: its message is written to be shown to a person. return `${err.message} (request ${err.requestId})`}Every rule the API can answer with is on errors and refusals, with what it means and
what to do. A rule you do not handle still has a message written for a person: show it, and log
the request_id, which Sotirea can trace if you ask about it.