Skip to content

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.

  1. Open a vault when a customer needs an address.

    Call POST /v1/vaults with your customer’s id as the reference, 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)
    }
    }
  2. Show them the address for their network.

    The vault’s addresses has 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 whose paused is true.

  3. Add a webhook for deposits.

    In the console, under Developers and Webhooks, add your server’s https address and choose deposit.received, and deposit.arriving if you want to tell customers their money is on its way. Keep the secret it shows you in your server’s environment.

  4. Credit the customer once, when it is final.

    Check every delivery, then credit the customer whose vault_reference the 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)
    }

    Record the deposit’s own id with 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 vaults
    case d.GasPool:
    return nil // coin for network fees, not a customer's
    case d.VaultReference == nil:
    return nil // a vault you did not open for a customer
    case 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 on received, never on arriving. An arriving deposit is not final and could still disappear. See deposits and finality.
  • Unrecognised tokens. A deposit with recognised false 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 held true 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 its id.