3 · Credit decision
Phase 3 of the flow, and the only fully asynchronous one. Nexa has a deposit
report; Nexa decides. The decision reaches Tesser as a webhook delivered to
[T] Tesser's receiver, which Nexa calls:
POST /v1/webhooks/nexa/{organizationId}: Receive Nexa webhook (op 4).
Step 4a · Nexa credits, with quote.credit.sent then quote.credit.confirmed [T]
Operation: POST /v1/webhooks/nexa/{organizationId}: Receive Nexa webhook (op 4)
· hosted by Tesser, called by Nexa.
Two events, in that order:
event_type | Meaning | data |
|---|---|---|
quote.credit.sent | Nexa decided to credit; the instruction is submitted to core banking. | tesser_quote_id, quote_status: "CREDIT_SENT", gtq_amount |
quote.credit.confirmed | GTQ is 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 |
The gtq_amount on quote.credit.confirmed is the number Tesser treats as
truth and the number it must send back in
create payment, where Nexa cross-checks
it there.
What credit.sent versus credit.confirmed mean inside Nexa's core banking,
and the expected gap between them, is open question 2.
Step 4b · Nexa requests more funds, with quote.additional_funds_requested [T]
Operation: POST /v1/webhooks/nexa/{organizationId}: Receive Nexa webhook (op 4)
· hosted by Tesser, called by Nexa.
event_type | Meaning | data |
|---|---|---|
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) |
This event does not change state. The quote stays DEPOSIT_REPORTED and
accumulates further reports while Nexa's decision is pending. Tesser's
response is another
deposit report [N] once the
extra USDC lands.
Because the event carries an action rather than a state advance, Tesser
processes it on the first delivery of its event_id regardless of the
reachability rule. See the webhooks guide.
Whether usdc_additional_requested is binding or indicative, and whether
Nexa can cancel the request and credit anyway, is
open question 9.
Step 4c · Nexa rejects, with quote.rejected [T]
Operation: POST /v1/webhooks/nexa/{organizationId}: Receive Nexa webhook (op 4)
· hosted by Tesser, called by Nexa.
event_type | Meaning | data |
|---|---|---|
quote.rejected | Nexa declines to credit. | tesser_quote_id, quote_status: "REJECTED", reason_code (RISK, AMOUNT or OTHER), note |
REJECTED is terminal. A new quote is the recovery path, and the deposit
endpoint returns 409 QUOTE_REJECTED from then on. The USDC has already
moved on-chain, and what happens to it is operational and out-of-band for the
POC (open question 4).
Nexa's decision rules (credit-as-quoted vs pro-rata vs request-more vs reject) and the SLA from deposit report to decision event are open question 3.
What can go wrong: webhook delivery
error_code | HTTP | When |
|---|---|---|
VALIDATION_ERROR | 400 | Malformed envelope, unknown event_type, bad data for the event type. |
UNAUTHENTICATED | 401 | No client certificate, unrecognised fingerprint, or the mTLS-resolved identity does not match {organizationId} in the path. |
INTERNAL_ERROR | 500 | Unexpected failure on Tesser's side. Retry this one. |
Operational failure modes that are not errors:
- Duplicate delivery. At-least-once means duplicates are expected. Tesser
dedupes on
event_idand still returns 2xx. - Out-of-order delivery. A stale event (current state or an ancestor) is acked and changes nothing.
- Conflicting event, for example
PAYMENT_FAILEDafterPAYMENT_COMPLETED, orquote.credit.sentafterquote.rejected. Tesser acks200and raises an ops alert: that is a data-integrity incident between the two companies, not a reordering, and silently ignoring it would hide real money divergence. - No decision at all. There is no timeout on Nexa's side of this phase in the POC. A quote that has had a deposit reported never auto-expires; if a decision does not arrive, it is an ops conversation.
Next: 4 · GTQ payout.