Going live

The pre-production checklist and verification steps required before moving your integration from sandbox to live.

The sandbox proves the flow. This page is what changes when the money is real, and what to have working before it is.

Swap the credential, not the code

A live key addresses the same organization id and the same endpoints. Only the prefix changes: akid_test_…akid_live_…. If anything else in your integration has to change, that is a bug on our side — tell us.

export AVVIO_API_KEY=akid_live_…
export AVVIO_PRIVATE_KEY='-----BEGIN PRIVATE KEY-----…'

The identifier is public; its private P-256 half authorizes requests. Keep the private key server-side. Our CORS policy does not allow the API header, so a browser cannot call the partner API directly.

What genuinely differs in production

SandboxLive
SettlementSecondsHours to days, per corridor
RatesFixedReal, and they move between quoting and sending
Balancesandbox/fundFunded by wire — see payin-accounts
Failure triggersAccount-number suffixWhatever actually happens
CorridorsA handfulWhat your routing supports — read the corridors call

The last row is the one that surprises people. The corridor list and the field names within it depend on how your organization is routed, and we may re-route you. A form built against hardcoded field names breaks on a routing change; a form built from GET /recipients/{orgId}/corridors does not.

The checklist

Before your first live payout

  • You persist your own Idempotency-Key before you send, and reuse it on every retry. This is the single thing that prevents a double payment. Generating one per attempt defeats it entirely.

  • A timeout is treated as an unknown outcome, not a failure. Retry with the same key; a replay returns the original payout.

  • Your ledger does not treat completed as final. A bank can return a settled payment days later. Exercise it in sandbox with account suffix 0003 before you go live, not after.

  • You read fundsReturned rather than inferring from failureCode. It is absent when we do not know, which is deliberately not the same as false.

  • You reconcile from GET /events, carrying nextSince, and dedupe on id.

    The request parameter is since, not nextSince. nextSince is the field we return; feed its value back as since:

    curl -s "$AVVIO_BASE_URL/payments/organizations/$AVVIO_ORG_ID/events?since=624" \
      -H "x-api-key: $AVVIO_API_KEY"

    Sending ?nextSince= instead replays your whole history on every poll, because unknown query parameters are ignored rather than rejected. That is the single easiest way to build a reconciler that silently reprocesses everything forever.

    Use /events. It gives one row per transition with a sequence cursor, and a sequence never changes once assigned.

    GET /orders?updatedSince= also returns changed payouts oldest-changed first, and it is fine for a human-facing list — but its sort key is updatedAt, which moves whenever a payout does. Page it while payouts are settling and rows can land behind a cursor you have already passed. Use it to look at recent activity, not as the thing your ledger depends on.

    (Two earlier versions of this paragraph were wrong in opposite directions: one said the payout list could not show a change to something you had already read — false with updatedSince — and one recommended /events because the payout list accepted a bad cursor silently, which is now the reverse of the truth: /orders rejects an unknown cursor with a 400.)

  • Your webhook receiver verifies signatures over the raw bytes, and you have tested that a wrong secret is rejected.

    The scheme is Standard Webhooks, so any Svix-compatible verifier works. If you are writing it yourself — and the quickstart explicitly courts non-Node shops — this is the whole algorithm:

    signed = "{svix-id}.{svix-timestamp}.{raw request body}"
    key    = base64_decode(secret without its "whsec_" prefix)
    expect = base64(HMAC_SHA256(key, signed))

    Compare against each space-separated entry in svix-signature after its v1, prefix, in constant time. Verify over the RAW bytes, before parsing. Reject timestamps outside ±5 minutes.

    It was previously documented only inside the OpenAPI file, which is the one place a non-Node integrator following this checklist would not look.

  • You treat webhooks as the nudge and the API as the truth.

Operationally

  • You know which corridors you actually need and have confirmed each one appears in GET /recipients/{orgId}/corridors for your organization.
  • You have a funded balance. Live balances are funded by wire; there is no live equivalent of sandbox/fund.
  • You store requestId. On an ERROR it is a body field; on a SUCCESS it is the x-request-id header and NOT in the body. Log the header and you have it either way — a logger reading res.body.requestId records undefined for every successful call. It is what lets us find your exact request.
  • You have somewhere for a human to look at a compliance_rejected payout. Those funds do not come back automatically.

Rate limits

Per credential, plus a per-source ceiling. The response carries x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset — read them rather than guessing, and back off on 429 (which is retryable).

If your volume needs a higher ceiling, tell us before you go live rather than discovering it in a payroll run.

Tell us before you scale

We would rather hear "we are about to send 5,000 payouts on Friday" than find out from a graph. Corridor limits, balance headroom and rate ceilings are all things we can raise, and none of them can be raised retroactively.


Did this page help you?