> For the complete documentation index, see [llms.txt](https://docs.nerovasystems.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.nerovasystems.com/guides/build-well/error-handling.md).

# Handle errors and retries

Build an integration that survives real-world failure: parse problem details the way the platform intends, retry only what is safe to retry, and hand support the one identifier that finds your request in seconds.

This guide applies the rules from [Conventions](https://docs.nerovasystems.com/documentation/api/conventions) — read that page first; this one turns it into working client behavior.

## What you'll build

A single error-handling layer for all your Nerova calls: one problem-details parser, one retry policy, one logging discipline. Every snippet works against the `/api/v1/**` surface today.

## Prerequisites

* A Live API key and one successful call from the [Quickstart](https://docs.nerovasystems.com/documentation/getting-started/quickstart).
* The status-code table from [Conventions](https://docs.nerovasystems.com/documentation/api/conventions#status-codes) at hand.

## Parse problems by `code`, not by text

Every non-success response is `application/problem+json` (RFC 9457) with two Nerova extensions: a stable machine-readable `code` and a `correlationId`. Branch on `status` and `code`; treat `title` and `detail` as display strings that can change without notice.

```json
{
  "type": "about:blank",
  "title": "Request could not be completed",
  "status": 409,
  "detail": "Correct the request and try again.",
  "code": "example.conflict",
  "correlationId": "request_example_001"
}
```

A robust parser needs three behaviors:

1. **Tolerate non-problem bodies.** Infrastructure between you and the API can return bare status codes. If the body isn't parseable problem JSON, fall back to the status code alone.
2. **Ignore unknown fields.** Additive change is allowed; new problem members must not break you.
3. **Capture `correlationId` and `X-Request-Id` on every failure**, before any retry logic runs.

The SDKs do all three for you and surface the result as one typed error — `NerovaApiException` in .NET, `NerovaApiError` in TypeScript — with the parsed problem, the stable `code`, the request id, and rate-limit state attached. See the [SDK guides](https://docs.nerovasystems.com/sdks).

## Decide: retry, repair, or stop

Classify by status before anything else:

| Status | Class                                           | Correct reaction                                    |
| ------ | ----------------------------------------------- | --------------------------------------------------- |
| `400`  | Your request is malformed                       | Fix the request. Retrying as-is always fails        |
| `401`  | Key missing, malformed, or revoked              | Repair the key; do not retry                        |
| `403`  | Key lacks a scope or tenant grant               | Fix scopes/grants in the console; do not retry      |
| `404`  | Unknown id — or a tenant your key isn't granted | Verify the id and the grant set; do not blind-retry |
| `409`  | State conflict                                  | Read the resource, then decide (below)              |
| `412`  | Version precondition failed                     | Re-read, retry with current `version` (below)       |
| `429`  | Rate limited                                    | Back off and retry (below)                          |
| `503`  | Temporarily unavailable                         | Retry with backoff                                  |

The rule of thumb: **retry `429` and `503`; repair and re-issue `409` and `412`; never auto-retry `400`, `401`, `403`, or `404`.** Auth, permission, environment, and validation failures do not fix themselves — a retry loop on those only burns quota and hides the bug.

```mermaid
flowchart TD
    R[Non-success response] --> C{status}
    C -->|429 or 503| RETRY["Back off, then retry<br/>with the same Idempotency-Key"]
    C -->|409 or 412| READ[Re-read the resource] --> REPAIR["Repair, then re-issue<br/>with the current version"]
    C -->|400, 401, 403, 404| STOP["Stop — fix the request,<br/>key, scopes, or grants first"]
    RETRY --> OK[Succeeded]
    REPAIR --> OK
```

Note that `404` is deliberately ambiguous: the platform fails closed, so a tenant outside your key's grant set looks identical to a tenant that doesn't exist. Before chasing a "missing" resource, check the key's grants — see [Grants make tenants visible](https://docs.nerovasystems.com/documentation/concepts/accounts-and-tenants#grants-make-tenants-visible).

## Make writes safe with idempotency keys

Every documented mutation takes a caller-generated `Idempotency-Key` (8–128 characters from `A–Z a–z 0–9 _ . : -`). Derive it from the *logical command* — your own entity id plus the action — never from the attempt:

```
provision-tenant-your-crm-id-123      ✓ one command, stable across retries
provision-tenant-{random-uuid}        ✗ new key per attempt = duplicate risk
```

With a stable key, a retry after a network failure replays the original command and returns the original result. Activation-flow responses even tell you which happened: `idempotencyStatus` is `Applied` on first execution and `Replayed` on a safe replay.

### When the outcome is unknown

A timeout *after* the request was transmitted means the write may or may not have happened. Do not immediately re-send with a **new** key — that is how duplicates are made. Instead:

1. Read the resource back (`GET`) and check whether the write landed.
2. If it didn't, re-send with the **same** key.
3. If you can't tell, re-sending with the same key is still safe — that is the point of the key.

The SDKs surface the server-reported ambiguous case explicitly: when a problem's `code` marks the outcome as unknown, `error.isOutcomeUnknown` is `true` in TypeScript and `NerovaApiException.IsOutcomeUnknown` in .NET. Transport-level timeouts never reach the API and surface as your HTTP stack's own error — treat those as unknown-outcome too and follow the same read-back-then-replay procedure.

## Conflicts: 409 and 412

Both codes mean "the world moved"; they differ in who noticed.

**`409 Conflict`** — the server rejected a command that contradicts current state: creating a connection that already exists, activating a tenant that isn't ready. Read the resource back and let its actual state pick your next step. A `409` on "create duplicate" is usually good news — the thing you wanted exists; fetch it and continue.

**`412 Precondition Failed`** — you sent `expectedVersion` (or a concurrency-checked body field) and the resource's `version` has moved on. The loop is mechanical:

```
loop (bounded, e.g. 3 attempts):
  current = GET the resource            # take current.version
  decide whether your change still makes sense against current state
  mutate with expectedVersion = current.version and the SAME Idempotency-Key…
     …if the command is unchanged; a NEW key if re-deciding produced a
       different command
  on 412: continue loop
```

If you exhaust the loop, a human or another worker is actively contending on the same resource — surface it instead of spinning.

## Back off on 429

A `429` may carry `Retry-After`. Honor it when present; otherwise use exponential backoff with jitter and a cap:

```
delay = min(cap, base * 2^attempt) * random(0.5, 1.5)   # e.g. base 1s, cap 60s
```

Two constraints from [Conventions](https://docs.nerovasystems.com/documentation/api/conventions#rate-limits):

* Bound your retries. An unbounded retry loop against a rate limit is an outage amplifier.
* Only retry the rate-limited call itself — do not replay the whole pipeline.

Successful responses expose rate-limit state programmatically through the SDKs (`response.rateLimit`), so a fleet-scale caller can shed load *before* hitting `429`.

## Log correlation IDs — and get faster support

Every response, success or failure, carries `X-Request-Id`; every problem document repeats it as `correlationId`. That identifier is how Nerova finds your exact request without you sharing payloads.

Make it a logging rule:

* Record `X-Request-Id` on every non-2xx response, next to your own command identifier and the idempotency key you sent.
* When you [contact support](https://docs.nerovasystems.com/documentation/resources/support), include the `correlationId`, the endpoint, the UTC timestamp, and the observed vs expected behavior. Never include secrets or personal data.

You may also *send* a safe `X-Request-Id` yourself to stitch Nerova calls into your own tracing — keep it free of secrets and personal data.

## Partner-safe failure codes

Failures that originate downstream — a provider rejecting a credential, an account-side provisioning step failing — surface as **stable, partner-safe codes**, not raw provider errors: `lastErrorCode` on the tenant (for example `account.tenant_unavailable`), `code` on activation `blockingReasons`, problem `code` extensions everywhere else. Two consequences:

* You can branch on these codes safely; they are contract, like field names.
* You will never see another platform's internals in them — and your failure details are equally invisible to others. See [Security and data ownership](https://docs.nerovasystems.com/documentation/security/data-ownership).

For the operational recovery loops these codes drive — replace credential and resume, reconcile after repair — see the [partner playbook](/guides/provision/partner-playbook.md#5-handle-failures-without-duplicating-work).

## Next steps

* Get the typed error surface for free: [SDK guides](https://docs.nerovasystems.com/sdks).
* Bake these policies into your provisioning pipeline: [Provision and activate tenants](/guides/provision/partner-playbook.md).
* Verify your handling before real merchants: [Go-live checklist](/guides/provision/go-live.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.nerovasystems.com/guides/build-well/error-handling.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
