Lifecycle
One state machine per quote. Every API response and every webhook in either
direction carries quote_status from this enum, so Tesser's logs and Nexa's
logs line up on the same vocabulary when something needs investigating.
Scope of the MVP
- USDC is the only crypto asset; GTQ the only fiat currency.
- One customer:
FELIX(closed enum). Generalising to acustomer_idis deferred. - Each customer has exactly one wallet and one bank account at Nexa.
- One fixed beneficiary. Nexa always pays the same destination, agreed out-of-band and never present in an API payload. That beneficiary is the payout partner, which performs the last-mile payout to the individual remittance receivers.
- Nexa is not involved in the last mile. No per-recipient beneficiary details, no destination objects, no beneficiary compliance fields. Nexa taking on last-mile remittance payouts is a later stage of the programme, out of scope here.
- One payment per quote. Deposits are repeatable per quote (required by the additional-funds flow), but the normal case is one.
- No pagination, no list endpoints. One environment.
Contract rules
Four rules decide most of this integration's edge cases.
- Money is always a decimal string. GTQ 2dp, USDC 6dp, price 6dp. Never a JSON number.
- Nexa computes every derived amount. Tesser echoes
usdc_amountand the credited GTQ; it never divides locally. - Reporting a deposit is not requesting anything. A
2xxmeans "recorded". Nexa's credit / more-funds / reject decision comes back by webhook. - Every POST is idempotent on a caller-minted ID. A retry returns the
current object; a conflicting body returns
409. Network retries can never false-fail.
The whole flow in one diagram
The five phase pages that follow each show their own slice of this diagram.
[N] = Nexa-hosted (Tesser calls it), [T] = Tesser-hosted (Nexa calls it);
in the diagram the same labels appear as [N] and [T].
Quote lifecycle (shared state vocabulary)
State table
quote_status | Set by | Terminal? | Meaning |
|---|---|---|---|
CREATED | Create quote [N] | no | Quote exists with a price and an expires_at. Not yet locked. |
ACCEPTED | Accept quote [N] | no | Rate locked. Nexa holds it until deposit_deadline. |
DEPOSIT_REPORTED | Report USDC deposit [N] | no | Tesser has attested at least one USDC delivery. Nexa owes a decision. |
CREDIT_SENT | quote.credit.sent webhook [T] | no | Nexa decided to credit; instruction submitted to core banking. |
CREDIT_CONFIRMED | quote.credit.confirmed webhook [T] | no | GTQ available in Felix's Nexa account. gtq_amount here is authoritative. |
PAYMENT_COMPLETED | Create payment [N], in the response | yes, unless a failure event follows | Payout made to the fixed beneficiary. |
PAYMENT_FAILED | Create payment [N] in the response (synchronous decline), or the payment.status.changed webhook [T] (failure after a COMPLETED response) | yes | Payout declined or failed. Recovery is manual in the POC. |
PAYMENT_RETURNED | payment.status.changed webhook [T] | yes | Payout returned by the rail. Recovery is manual in the POC. |
EXPIRED | time | yes | Acceptance deadline or deposit deadline passed with nothing reported. |
REJECTED | quote.rejected webhook [T] | yes | Nexa declines to credit. The USDC has already moved; see below. |
Rules the diagram encodes
EXPIREDis reachable only fromCREATED(acceptance deadline missed) andACCEPTED(no deposit reported bydeposit_deadline). Once any deposit is reported in time, the quote never auto-expires, and Nexa owes a decision.REJECTEDis Nexa's decision after a deposit report (via thequote.rejectedwebhook), never a synchronous deposit-call error. After rejection,GET /v1/quotes/{tesser_quote_id}returnsquote_status: "REJECTED".- An additional-funds request does not change state: the quote stays
DEPOSIT_REPORTEDand accumulates deposit reports while Nexa's decision is pending. Late reports after a decision are also accepted. - Terminal states are
EXPIRED,REJECTED,PAYMENT_COMPLETED,PAYMENT_FAILEDandPAYMENT_RETURNED. Recovery fromPAYMENT_FAILED/PAYMENT_RETURNED, and disposition of USDC afterREJECTED, are manual and out-of-band in the POC (open questions 4 and 7). - The payout is synchronous. The create payment response carries the
outcome:
PAYMENT_COMPLETED, orPAYMENT_FAILEDwhen Nexa declines or cannot execute the payout during the call. There is no success webhook, and the only payout event is a failure that surfaces on the rail after aCOMPLETEDresponse — which is whyPAYMENT_COMPLETEDis the one terminal state that can still be left, moving toPAYMENT_FAILEDorPAYMENT_RETURNED. How long after aCOMPLETEDresponse such a failure can still surface is open question 5.
Rejection is not an undo
The USDC has already moved on-chain before the deposit endpoint is ever
called. A quote.rejected decision means "Nexa will not credit GTQ", not "the
deposit didn't happen". What happens to the received USDC is operational and
out-of-band for the POC. It is called out here because it is the edge most
likely to cause a cross-company incident.
Glossary
| Term | Meaning |
|---|---|
tesser_quote_id | UUIDv4 minted by Tesser before the create-quote call. Doubles as the idempotency key and is the cross-reference for every later call and webhook. |
nexa_quote_id | Nexa's own reference for the quote, returned for support and reconciliation. |
tesser_deposit_id | UUIDv4 minted by Tesser per deposit report; idempotency key for that report. |
nexa_payment_id | Nexa's identifier for the GTQ payout. |
quote_status | The shared state machine value carried on every response and webhook. |
payment_status | The payout's outcome. COMPLETED or FAILED can both come back synchronously on create payment; RETURNED, and a FAILED that only surfaces after a COMPLETED response, arrive via the failure webhook. |
usdc_gtq_price | GTQ per 1 USDC, string, max 6 decimal places. |
quoted_gtq | The GTQ Nexa will actually provide. May be less than desired_gtq (a partial fill). |
usdc_amount | USDC Felix must deliver: quoted_gtq / usdc_gtq_price, rounded up at 6dp. Computed by Nexa. |
| Custody | Nexa's BitGo wallet, whose keys Tesser holds. Felix's USDC lands here. |
Worked example, used in every example and every stub response in this
portal: Felix wants GTQ 250000.00; Nexa quotes 7.745100 GTQ per USDC; so
usdc_amount is 32278.472841 (rounded up at 6dp). Checking the round-up:
32278.472841 × 7.745100 = 250000.0000008…, which covers quoted_gtq.