# 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)](/api-tesser/webhooks#receive-nexa-webhook-op-4).

<Mermaid
  chart={`sequenceDiagram
    autonumber
    participant T as Tesser (orchestrator)
    participant N as Banco Nexa

    Note over T,N: Phase 3 · Nexa's credit decision (asynchronous)
    alt Nexa credits
        N->>T: webhook quote.credit.sent [T]
        N->>T: webhook quote.credit.confirmed (authoritative gtq_amount) [T]
    else Nexa requests more funds
        N->>T: webhook quote.additional_funds_requested [T]
        T->>N: POST .../deposits (another report) [N]
    else Nexa rejects
        N->>T: webhook quote.rejected [T]
    end`}
/>

## Step 4a · Nexa credits, with `quote.credit.sent` then `quote.credit.confirmed` <T />

**Operation:** [`POST /v1/webhooks/nexa/{organizationId}`: Receive Nexa webhook (op 4)](/api-tesser/webhooks#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](/api-nexa/payments#create-payment-op-5), 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](/open-questions).

## Step 4b · Nexa requests more funds, with `quote.additional_funds_requested` <T />

**Operation:** [`POST /v1/webhooks/nexa/{organizationId}`: Receive Nexa webhook (op 4)](/api-tesser/webhooks#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](/api-nexa/deposits#report-usdc-deposit-op-3) <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](/webhooks-guide).

Whether `usdc_additional_requested` is binding or indicative, and whether
Nexa can cancel the request and credit anyway, is
[open question 9](/open-questions).

## Step 4c · Nexa rejects, with `quote.rejected` <T />

**Operation:** [`POST /v1/webhooks/nexa/{organizationId}`: Receive Nexa webhook (op 4)](/api-tesser/webhooks#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](/open-questions)).

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](/open-questions).

<div className="what-can-go-wrong">

**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_id` and 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_FAILED` after
  `PAYMENT_COMPLETED`, or `quote.credit.sent` after `quote.rejected`. Tesser
  acks `200` **and 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.

</div>

**Next:** [4 · GTQ payout](/flow-nexa/gtq-payout).
