Accept deposits per customer
The usual shape for an app that holds money for its customers: each customer gets a vault of their own, named by your id for them, and your server credits them when Sotirea says their money is final.
-
Open a vault when a customer needs an address.
Call
POST /v1/vaultswith your customer’s id as thereference, whenever they ask where to pay. The first call opens their vault; every later call answers the same one, so you never have to remember whether you did. See vaults and addresses.vault, _, err := client.CreateVault(ctx, sotirea.VaultRequest{Reference: "cust_1001",})if err != nil {log.Fatal(err)}for _, a := range vault.Addresses {if a.ChainID == sepolia {fmt.Printf("Pay vault %d at %s\n", vault.ID, a.Address)}}const vault = await sotirea.createVault({ reference: 'cust_1001' })for (const a of vault.addresses) {if (a.chain_id === sepolia) {console.log(`Pay vault ${vault.id} at ${a.address}`)}} -
Show them the address for their network.
The vault’s
addresseshas one entry per network, and the address is the same on every one. Say which network and which asset you expect, because money sent on a network Sotirea does not support cannot be seen. Do not show an address whosepausedis true. -
Add a webhook for deposits.
In the console, under Developers and Webhooks, add your server’s https address and choose
deposit.received, anddeposit.arrivingif you want to tell customers their money is on its way. Keep the secret it shows you in your server’s environment. -
Credit the customer once, when it is final.
Check every delivery, then credit the customer whose
vault_referencethe deposit carries:func receive(w http.ResponseWriter, r *http.Request) {// Read the body exactly as it arrived: the signature covers these bytes.body, err := io.ReadAll(io.LimitReader(r.Body, 1<<20))if err != nil {http.Error(w, "unreadable", http.StatusBadRequest)return}secret := os.Getenv("SOTIREA_WEBHOOK_SECRET")event, err := sotirea.VerifyWebhook(secret, r.Header, body)if err != nil {// Not from Sotirea, altered, or too old: never act on it.http.Error(w, "not verified", http.StatusUnauthorized)return}if event.Type == sotirea.EventDepositReceived {d, err := event.Deposit()if err != nil {http.Error(w, "unreadable", http.StatusBadRequest)return}if err := credit(d); err != nil {// Anything but a 2xx and Sotirea tries again later.http.Error(w, "try again", http.StatusServiceUnavailable)return}}w.WriteHeader(http.StatusNoContent)}export async function receive(request: Request): Promise<Response> {// Read the body exactly as it arrived: the signature covers these bytes.const body = await request.text()const secret = process.env.SOTIREA_WEBHOOK_SECRET ?? ''let event: Awaited<ReturnType<typeof verifyWebhook>>try {event = await verifyWebhook(secret, request.headers, body)} catch {// Not from Sotirea, altered, or too old: never act on it.return new Response('not verified', { status: 401 })}if (event.type === 'deposit.received') {try {await credit(depositEvent(event))} catch {// Anything but a 2xx and Sotirea tries again later.return new Response('try again', { status: 503 })}}return new Response(null, { status: 204 })}Record the deposit’s own
idwith the credit, in the same transaction, so a repeated webhook never credits twice, and skip what is not a customer’s money:// credit adds a final deposit to the customer whose vault it// arrived in.func credit(d sotirea.DepositEvent) error {switch {case d.Sweep:return nil // your own money moving between your vaultscase d.GasPool:return nil // coin for network fees, not a customer'scase d.VaultReference == nil:return nil // a vault you did not open for a customercase d.Held:// Credited by Sotirea but held for a compliance review: not// spendable, and Sotirea will contact you. Record it as held.return ledger.hold(*d.VaultReference, d.ID, d.Asset, d.Amount)}// d.ID is the deposit's own id: record it with the credit, in the// same transaction, so a repeated webhook never credits twice.return ledger.credit(*d.VaultReference, d.ID, d.Asset, d.Amount)}// credit adds a final deposit to the customer whose vault it// arrived in.async function credit(d: DepositEvent): Promise<void> {if (d.sweep) return // your own money moving between your vaultsif (d.gas_pool) return // coin for network fees, not a customer'sif (d.vault_reference === null) return // a vault you did not open for a customerif (d.held) {// Credited by Sotirea but held for a compliance review: not// spendable, and Sotirea will contact you. Record it as held.return ledger.hold(d.vault_reference, d.id, d.asset, d.amount)}// d.id is the deposit's own id: record it with the credit, in the// same transaction, so a repeated webhook never credits twice.return ledger.credit(d.vault_reference, d.id, d.asset, d.amount)}
What to watch for
Section titled “What to watch for”- Credit on
received, never onarriving. An arriving deposit is not final and could still disappear. See deposits and finality. - Unrecognised tokens. A deposit with
recognisedfalse is a token Sotirea does not recognise, whose symbol can imitate a real one. Decide whether to credit it by its contract (asset), never by its symbol. - Held deposits. A deposit with
heldtrue came from an address a sanctions list names. It is in the vault, but held for review and not spendable; Sotirea will contact you. - Missed webhooks. If your server was down for longer than Sotirea retries, list what came in
with
GET /v1/deposits?vault=and credit anything you have not recorded, by itsid.