> ## Documentation Index
> Fetch the complete documentation index at: https://yn-c9bb3266.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Error handling

> The Yuno error envelope, the real codes services return, and a sane retry strategy. Verified against the production codebase.

Every Yuno API error returns the same compact JSON envelope. Parse the `code` for programmatic handling and surface the `messages` array to your support tools.

## Error envelope

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  "code": "CUSTOMER_ID_DUPLICATED",
  "messages": [
    "A customer with this merchant_customer_id already exists for this account."
  ]
}
```

| Field      | Type            | Description                                                                                                                                                    |
| ---------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`     | string          | Stable, machine readable identifier in `SCREAMING_SNAKE_CASE`. Branch on this in your code.                                                                    |
| `messages` | array of string | Human readable details. Always an array, even when there is only one entry. Validation errors put one entry per failed field, formatted `"fieldName message"`. |

There is no `error` wrapper, no singular `message`, no `type`, no `details`. The body is exactly the two top level fields above.

<Note>
  Yuno sets `x-trace-id` on every response. When you open a support ticket, attach the response body **and** the `x-trace-id` header so the team can pull the trace.
</Note>

## HTTP status to error code

The shared utility libraries map common HTTP statuses to a default `code`. Service code may return a more specific business `code` for the same status (see the next table).

| HTTP  | Default `code`             | Meaning                                                                                   |
| ----- | -------------------------- | ----------------------------------------------------------------------------------------- |
| `400` | `BAD_REQUEST`              | Generic bad request fallback when no business code applies.                               |
| `400` | `VALIDATION_ERROR`         | Request payload failed schema or constraint validation.                                   |
| `400` | `INVALID_REQUEST`          | Request was rejected by the resource's business rules.                                    |
| `401` | `UNAUTHORIZED`             | Missing or invalid `public-api-key` / `private-secret-key`.                               |
| `403` | `FORBIDDEN`                | Caller authenticated but is not allowed to perform the action.                            |
| `404` | `NOT_FOUND`                | The route does not exist, or the framework catch all could not match the resource.        |
| `405` | `METHOD_NOT_ALLOWED`       | The HTTP verb is not supported on this path. (Kotlin services emit `UNSUPPORTED_METHOD`.) |
| `408` | `REQUEST_TIMEOUT`          | Request timed out before the service could respond.                                       |
| `413` | `REQUEST_ENTITY_TOO_LARGE` | Request body exceeds the service limit.                                                   |
| `415` | `UNSUPPORTED_MEDIA_TYPE`   | `Content-Type` is missing or not JSON.                                                    |
| `429` | `TOO_MANY_REQUESTS`        | Edge throttling kicked in. Back off and retry.                                            |
| `500` | `INTERNAL_ERROR`           | Unhandled server side error. Safe to retry idempotent operations.                         |
| `502` | `BAD_GATEWAY`              | Upstream provider was unreachable or returned a malformed response.                       |
| `503` | `SERVICE_UNAVAILABLE`      | Yuno service is overloaded or in maintenance.                                             |

<Warning>
  Yuno does not currently use HTTP `422`. Schema and constraint failures arrive as `400` with code `VALIDATION_ERROR` (Kotlin services) or `BAD_REQUEST` (Go services). Most "not found" business cases (such as `CUSTOMER_NOT_FOUND`, `RECIPIENT_NOT_FOUND`) also arrive as `400` with a descriptive code, not as `404`.
</Warning>

## Resource specific business codes

Services emit their own `code` values for domain rules. The most common ones you will see in production:

### Customers

| `code`                    | HTTP  | Meaning                                                                                                                                                                           |
| ------------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CUSTOMER_ID_DUPLICATED`  | `400` | A customer already exists with the same `merchant_customer_id`. Use [Retrieve Customer by External ID](/api-reference/customers/get-by-external-id) to fetch the existing record. |
| `CUSTOMER_NOT_FOUND`      | `400` | No customer matches the supplied identifier.                                                                                                                                      |
| `INVALID_PARAMETERS`      | `400` | One or more body parameters are invalid. The `messages` array names each failure.                                                                                                 |
| `CONCURRENT_MODIFICATION` | `409` | The record was updated by another caller while your request was in flight. Re fetch and retry.                                                                                    |

### Recipients (marketplace)

| `code`                          | HTTP  | Meaning                                                                                        |
| ------------------------------- | ----- | ---------------------------------------------------------------------------------------------- |
| `RECIPIENT_NOT_FOUND`           | `400` | No recipient matches the supplied identifier.                                                  |
| `EXTERNAL_ID_EXIST`             | `400` | A recipient already exists with the same `merchant_recipient_id`.                              |
| `INVALID_STATE`                 | `400` | The recipient cannot accept this action in its current state.                                  |
| `ONBOARDING_ALREADY_EXISTS`     | `400` | An onboarding flow has already started for this recipient.                                     |
| `ONBOARDING_NOT_FOUND`          | `400` | No onboarding record matches the supplied identifier.                                          |
| `TRANSFER_NOT_FOUND`            | `400` | No transfer matches the supplied identifier.                                                   |
| `INVALID_TRANSFER_STATUS`       | `400` | The transfer cannot accept this action in its current state.                                   |
| `ONBOARDING_RECIPIENT_MISMATCH` | `400` | The onboarding does not belong to the recipient supplied in the request.                       |
| `REQUEST_CANNOT_BE_PROCESSED`   | `400` | The request is structurally valid but cannot be processed against the current recipient state. |

### Validation framework codes (Kotlin services)

These come from the shared exception handler in the Kotlin services and apply to checkout sessions, recipients, payouts, and any service that uses the shared library.

| `code`              | HTTP  | Trigger                                                                                                                                 |
| ------------------- | ----- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `VALIDATION_ERROR`  | `400` | A required field is missing, an enum value is wrong, or a JSON field has the wrong type. The `messages` array lists each field failure. |
| `MISSING_HEADER`    | `400` | A required header (e.g., `public-api-key` or `private-secret-key`) was not sent.                                                        |
| `MISSING_PARAMETER` | `400` | A required query parameter was not sent.                                                                                                |
| `INVALID_DATE_TYPE` | `400` | A date field could not be parsed in the expected ISO format.                                                                            |
| `ILLEGAL_ARGUMENT`  | `400` | An argument violates a business invariant outside JSON validation.                                                                      |

## Provider errors

Errors that originate at a downstream payment provider are surfaced under codes prefixed with `PROVIDER_`. Examples include `PROVIDER_INVALID_CREDENTIALS`, `PROVIDER_INVALID_REQUEST`, `PROVIDER_PAYMENT_NOT_FOUND`, `PROVIDER_INVALID_AMOUNT`, `PROVIDER_COUNTRY_NOT_SUPPORTED`, and `PROVIDER_CURRENCY_NOT_ALLOWED`. The provider's raw response is reflected through `messages`. See [Provider errors](/troubleshooting/provider-errors) for the full mapping.

## Validation errors in practice

Validation failures from Kotlin services package every failed field into the `messages` array. The shape is always `{ code, messages }`, never a structured `details` object.

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  "code": "VALIDATION_ERROR",
  "messages": [
    "amount must be greater than 0",
    "country must not be blank",
    "merchant_order_id must not be blank"
  ]
}
```

When you display these to internal users (operators, support), surface every entry in `messages` so they can act on all failures at once.

## Retry strategy

<Note>
  For non idempotent writes, the safe pattern is to set a unique business key in the request body (`merchant_customer_id`, `merchant_order_id`, `merchant_recipient_id`). Yuno's database constraints reject duplicates, so a retry that finds the same key returns a clear `400` rather than creating a duplicate. The one endpoint that supports `X-Idempotency-Key` directly is `POST /v1/subscriptions`. See [Avoiding duplicates](/core-concepts/idempotency).
</Note>

Retry **transient** failures only. Do not retry codes that mean "your request is wrong". Automated retries on those waste budget and add noise.

| Retry                     | Do not retry                                               |
| ------------------------- | ---------------------------------------------------------- |
| `429 TOO_MANY_REQUESTS`   | `400 BAD_REQUEST` and `400 VALIDATION_ERROR`               |
| `500 INTERNAL_ERROR`      | `400 INVALID_REQUEST`                                      |
| `502 BAD_GATEWAY`         | `401 UNAUTHORIZED`                                         |
| `503 SERVICE_UNAVAILABLE` | `403 FORBIDDEN`                                            |
| `504 Gateway Timeout`     | `404 NOT_FOUND`                                            |
| `408 REQUEST_TIMEOUT`     | `409 CONCURRENT_MODIFICATION` (re fetch first, then retry) |

Use exponential backoff with full jitter, starting at 1 s and capped at 30 s. When `Retry-After` is present on a `429`, honor it as the floor for the wait.

<CodeGroup>
  ```javascript Node.js theme={"theme":{"light":"github-dark","dark":"github-dark"}}
  const RETRYABLE = new Set([408, 429, 500, 502, 503, 504]);

  async function retryWithBackoff(call, { maxRetries = 4 } = {}) {
    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        return await call();
      } catch (err) {
        if (!RETRYABLE.has(err.status) || attempt === maxRetries) throw err;

        const cap = Math.min(1000 * 2 ** attempt, 30_000);
        const jittered = Math.floor(Math.random() * cap);
        const retryAfterMs = Number(err.headers?.["retry-after"]) * 1000 || 0;
        const delay = Math.max(jittered, retryAfterMs);

        await new Promise((resolve) => setTimeout(resolve, delay));
      }
    }
  }
  ```

  ```python Python theme={"theme":{"light":"github-dark","dark":"github-dark"}}
  import random
  import time

  RETRYABLE = {408, 429, 500, 502, 503, 504}

  def retry_with_backoff(call, max_retries: int = 4):
      for attempt in range(max_retries + 1):
          try:
              return call()
          except YunoHTTPError as err:
              if err.status not in RETRYABLE or attempt == max_retries:
                  raise

              cap = min(1.0 * 2 ** attempt, 30.0)
              jittered = random.random() * cap
              retry_after = float(err.headers.get("retry-after", 0))
              delay = max(jittered, retry_after)

              time.sleep(delay)
  ```

  ```go Go theme={"theme":{"light":"github-dark","dark":"github-dark"}}
  var retryable = map[int]struct{}{
      408: {}, 429: {}, 500: {}, 502: {}, 503: {}, 504: {},
  }

  func RetryWithBackoff(call func() (*http.Response, error), maxRetries int) (*http.Response, error) {
      var lastErr error
      for attempt := 0; attempt <= maxRetries; attempt++ {
          resp, err := call()
          if err == nil && resp.StatusCode < 400 {
              return resp, nil
          }

          status := 0
          if resp != nil {
              status = resp.StatusCode
          }
          if _, ok := retryable[status]; !ok || attempt == maxRetries {
              return resp, err
          }
          lastErr = err

          cap := time.Duration(math.Min(float64(time.Second<<attempt), float64(30*time.Second)))
          jittered := time.Duration(rand.Int63n(int64(cap)))
          retryAfter := parseRetryAfter(resp.Header.Get("Retry-After"))
          delay := jittered
          if retryAfter > delay {
              delay = retryAfter
          }
          time.Sleep(delay)
      }
      return nil, lastErr
  }
  ```
</CodeGroup>

## Best practices

1. **Branch on `code`, never on `messages`.** The strings in `messages` are subject to copy edits and localization. The `code` is the contract.
2. **Set a unique business key** on every non idempotent write (`merchant_customer_id`, `merchant_order_id`, `merchant_recipient_id`). On `POST /v1/subscriptions` you can also pass `X-Idempotency-Key`.
3. **Log the full envelope plus the `x-trace-id` response header** for every failed request. Without `x-trace-id`, support cannot find your request in our traces.
4. **Treat `PROVIDER_*` codes as upstream signals.** They indicate the failure happened at the provider, not at Yuno. Inspect `messages` for the provider's raw text.
5. **Alert on `5xx` rate, not on individual `5xx` events.** Provider blips are normal. Sustained spikes are not.

## What next

<div className="mdx-card-tiles">
  <CardGroup cols={2}>
    <Card title="Avoiding duplicates" icon="rotate" href="/core-concepts/idempotency">
      Use unique business keys to make retries safe across customers, payments, and subscriptions.
    </Card>

    <Card title="Error codes catalog" icon="list" href="/troubleshooting/error-codes">
      Full reference of every error code Yuno can return.
    </Card>

    <Card title="Provider errors" icon="plug" href="/troubleshooting/provider-errors">
      `PROVIDER_*` codes mapped to each provider's raw responses.
    </Card>
  </CardGroup>
</div>
