Webhooks guide
Webhooks carry every asynchronous decision in this integration. There are two streams, and this page covers both.
| Stream | Receiver | Operation |
|---|---|---|
| Nexa to Tesser, credit decisions and payout failures | Tesser [T] | POST /v1/webhooks/nexa/{organizationId} |
| Tesser to Nexa, withdrawal status for treasury | Nexa [N] | POST /v1/webhooks/tesser |
Nexa → Tesser
Envelope
Every Nexa→Tesser event uses the same envelope:
Code
event_idis a UUID minted by Nexa. It is the dedupe key.occurred_atis RFC 3339 UTC.datavaries byevent_type, below.
Event catalogue
event_type | Meaning | data |
|---|---|---|
quote.credit.sent | Nexa decided to credit; instruction submitted to core banking | tesser_quote_id, quote_status: "CREDIT_SENT", gtq_amount |
quote.credit.confirmed | GTQ available in Felix's Nexa account | tesser_quote_id, quote_status: "CREDIT_CONFIRMED", gtq_amount (authoritative, and may deviate from quoted_gtq per Nexa's decision), credited_at |
quote.additional_funds_requested | Nexa wants more USDC before crediting | tesser_quote_id, quote_status: "DEPOSIT_REPORTED", usdc_total_reported, usdc_additional_requested, note (free text) |
quote.rejected | Nexa declines to credit | tesser_quote_id, quote_status: "REJECTED", reason_code (RISK, AMOUNT or OTHER), note |
payment.status.changed | The payout failed or was returned after Nexa already answered COMPLETED. Sent only on failure; there is no success event | tesser_quote_id, nexa_payment_id, payment_status (FAILED or RETURNED), quote_status, status_metadata (free-form object: rail references, failure reasons) |
The payout itself is synchronous: POST /v1/payments returns the
outcome, so a successful payout produces no event at all. See
4 · GTQ payout.
status_metadata appears only on payment.status.changed, where rail
references and failure reasons genuinely vary.
Delivery contract
At-least-once. Tesser dedupes on event_id. Nexa retries non-2xx with
exponential backoff, proposed up to 24 hours. Nexa's actual sender policy is
open question 6.
Success = any 2xx with an empty body. Non-2xx or a timeout means retry.
Ordering is not guaranteed. The reachability rule (state advancement
only) applies. The state machine defines which states are
reachable from which. For the state-advancement aspect of each event, the
receiver compares the event's quote_status to the current state:
Relationship of the event's quote_status to the current state | Receiver behaviour |
|---|---|
| Forward-reachable from current | Apply it. This is a genuine advance. |
| The current state itself, or an ancestor | Ack 200, change nothing. Stale duplicate or reorder. |
A different branch or a conflicting terminal, for example quote.credit.sent after quote.rejected, or PAYMENT_FAILED after REJECTED | Ack 200 and raise an ops alert. This is a data-integrity incident between the two companies, not a reordering. Silently ignoring it would hide real money divergence. |
The late-failure event is not a conflict. PAYMENT_FAILED and
PAYMENT_RETURNED are forward-reachable from PAYMENT_COMPLETED, so a
failure arriving after a successful payout response is handled by the normal
forward-reachable clause: apply it, no alert. It is the expected late-failure
path, and it is the only payout event there is.
Non-state-advancing events are still processed. Some events carry an
action or new information without moving the state machine:
quote.additional_funds_requested (the quote stays DEPOSIT_REPORTED, but
Tesser must act) and any payment.status.changed that repeats a failure
already recorded. These are processed on the first delivery of each event_id;
later deliveries of the same event_id are deduped as usual. The
reachability rule governs only the quote_status field, never whether the
event's content is handled.
Authentication
Nexa presents its client certificate; Tesser resolves Nexa's identity from
the registered fingerprint, and the resolved identity must match the
{organizationId} in the path. Mismatch is 401. Full detail on
Authentication.
Connectivity and credentials can be checked without sending an event:
HEAD /v1/webhooks/nexa/{organizationId}
[T].
Tesser → Nexa
Nexa hosts
POST /v1/webhooks/tesser
[N] to receive Tesser's standard outbound webhooks. In this POC the
subscription Nexa needs is withdrawal status updates, supporting
POST /v1/treasury/withdrawals
[T] in phase 5.
The envelope and event types are Tesser's existing ones. This portal documents which subscription Nexa needs and links to docs.tesser.xyz for the event-type strings rather than re-specifying a contract that already exists and is already live elsewhere.
Delivery auth: Ed25519 X-Tesser-Signature today, plus a Tesser client
certificate once the sender's outbound-mTLS work lands. See
Authentication.
Checklist for implementers (both directions)
- Dedupe on the event id before doing anything else. At-least-once means duplicates are normal traffic, not an incident.
- Ack fast, process after. Any 2xx with an empty body ends the delivery attempt. Do not hold the connection open for downstream work.
- Return 5xx only when a retry would help. A malformed payload is
400and will never succeed; retrying it wastes both sides' time. - Never derive state from arrival order. Compare
quote_statusagainst your current state and apply the reachability rule. - Alert on conflicting events rather than dropping them. A conflicting terminal means the two companies disagree about money.
- Keep a recovery path. If an event is lost,
GET /v1/quotes/{tesser_quote_id}[N] andGET /v1/payments/{nexa_payment_id}[N] answer "what state do you think it's in?" in one call.