# Error Responses

> Every failing request returns the same JSON Error object. Reference for the Type values, their HTTP status mapping, and the extensions.

## Overview

When a Business API request fails, the response body is a JSON `Error` object with a consistent shape across every endpoint. The `Type` field tells you the category of failure and maps to the HTTP status code:

| `Type`                | HTTP  | When it happens                                                         |
| --------------------- | ----- | ----------------------------------------------------------------------- |
| `InvalidMessage`      | `400` | The request is malformed or a field fails validation.                   |
| `Unauthorized`        | `401` | The API key is missing, invalid, or expired, or request signing failed. |
| `Forbidden`           | `403` | A policy or compliance rule blocks the action.                          |
| `ResourceNotFound`    | `404` | A referenced resource does not exist.                                   |
| `InsufficientBalance` | `402` | The account balance is below the requested amount.                      |
| `Unexpected`          | `500` | An unexpected server-side error occurred.                               |

## Error envelope

Every error response includes these fields:

| Field              | Description                                                                                                               |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------- |
| `Type`             | Error category: `InvalidMessage`, `Forbidden`, `InsufficientBalance`, `ResourceNotFound`, `Unauthorized`, or `Unexpected` |
| `Action`           | Internal action name for the operation that failed (for example `create customer`, `withdraw`)                            |
| `Detail`           | Human-readable summary of the failure                                                                                     |
| `Instance`         | Unique ID for this occurrence — use it to correlate a failure with server logs when contacting support                    |
| `RequestExtension` | Field-level validation details (see below)                                                                                |
| `DenyExtension`    | Policy or compliance denial details (see below)                                                                           |

### RequestExtension

Field-level problems are returned in `RequestExtension.Body` as an array of items:

| Field         | Description                                                                                |
| ------------- | ------------------------------------------------------------------------------------------ |
| `Field`       | Dot path to the request field (for example `CustomerID`, `Form.BankDetails.AccountNumber`) |
| `Reason`      | Why the field failed — see the values below                                                |
| `Description` | What failed and, where possible, how to fix it                                             |

`Reason` is one of:

| Reason                | Meaning                                                                                                                   |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| `SchemaValidation`    | Value fails the JSON Schema or a business rule (wrong type, length, pattern, required).                                   |
| `IntegrityValidation` | Cross-field or state conflict (for example a referenced resource does not exist, or a form session expired).              |
| `NotSupported`        | The value is well-formed but not supported for this operation (for example an IBAN that does not support SEPA transfers). |
| `SelfReference`       | The request points back at the caller in a way that is not allowed (for example withdrawing to your own deposit address). |

On an `InvalidMessage` response with `RequestExtension`, read each item's `Reason` and follow the matching remediation path:

| `Reason`              | What it means                                                                    | What to do                                                                                |
| --------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `NotSupported`        | The value is valid but not supported for the selected channel or payment method. | [Form field not supported](#form-field-not-supported)                                     |
| `SchemaValidation`    | The value fails schema or business-rule constraints.                             | Fix the field using `Description` and the channel or onboarding JSON Schema.              |
| `IntegrityValidation` | A referenced resource is missing or a session has expired.                       | Resolve the state conflict (for example create the resource or start a new form session). |
| `SelfReference`       | The request references the caller in a disallowed way.                           | Use a different destination that is not owned by the caller.                              |

### DenyExtension

Policy denials include `Reason` (`InsufficientPermissions`, `Compliance`, `AccountStatus`, or `VerificationRequired`), `Description`, and the `Principal` / `Resource` involved.

On a `Forbidden` response, read each `DenyExtension` item's `Reason` and follow the matching remediation path:

| `Reason`                  | What it means                                                                                                       | What to do                                                        |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
| `Compliance`              | A policy or regulatory rule blocks the action (for example channel policy, travel rule, or geographic restriction). | [Channel and compliance denials](#channel-and-compliance-denials) |
| `VerificationRequired`    | Identity or account verification is incomplete. Check `Principal.Type` to see whose verification is missing.        | [Verification required](#verification-required)                   |
| `InsufficientPermissions` | The feature, currency, or network is not enabled for your business profile.                                         | [Insufficient permissions](#insufficient-permissions)             |
| `AccountStatus`           | The account is suspended or otherwise restricted.                                                                   | [Account status](#account-status)                                 |

For `VerificationRequired`, `Principal.Type` determines the next step:

| `Principal.Type` | Who needs verification          | What to do                                                                                                 |
| ---------------- | ------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `Customer`       | The end customer                | Initiate or resume [hosted onboarding](/api-reference/create-onboarding-session)                           |
| `User`           | Your business profile (KYB/KYC) | Complete verification through the Business Dashboard or your Noah representative — not customer onboarding |

## Form field errors

Some endpoints accept a `Form` object for payment or onboarding data. Those fields are validated against a **JSON Schema** — either a channel form schema for payouts or an onboarding schema for KYC.

The set of possible form field errors is **not fixed globally**. It depends on the channel, payment method, country, provider, and schema version. The same field can produce different `Description` text on different channels.

To reduce opaque server-side failures:

1. Retrieve the relevant schema before submitting (for example [GET `/channels/{ChannelID}/form`](/api-reference/dynamic-form) for payout forms).
2. Validate `Form` client-side against that JSON Schema.
3. On `400`, read `RequestExtension.Body` and map each `Field` to your UI controls to show inline errors.

Published schemas are available at [github.com/noah-labs/public-schemas](https://github.com/noah-labs/public-schemas). See [Form Schemas](./form-schemas.md) for details.

:::tip

`SchemaValidation` errors on form fields usually mirror the schema's constraints and `errorMessage` values. Treat the schema as the source of truth for field-level messages, not a static error catalogue in these docs.

:::

## Multi-step responses are not errors

Some form-session flows return HTTP `200` with a `NextStep` object when additional acknowledgement or data is required (for example VoP or CoB). This is a **continuation**, not a failure. Call the same endpoint again with the returned `FormSessionID` and the fields requested in `NextStep.Schema`.

## Examples

The examples below illustrate common error shapes across the API. `Action`, `Detail`, and field paths vary by endpoint.

### Required field missing

```json
{
  "Type": "InvalidMessage",
  "Action": "create customer",
  "Detail": "invalid request",
  "Instance": "9bbe5ce1-aa6a-4650-ae2b-084f4e1f229c",
  "RequestExtension": {
    "Body": [
      {
        "Field": "CustomerID",
        "Reason": "SchemaValidation",
        "Description": "required"
      }
    ]
  }
}
```

### Form field validation

Invalid value for a schema-backed form field (exact `Description` varies by channel):

```json
{
  "Type": "InvalidMessage",
  "Action": "validate form",
  "Detail": "invalid request",
  "Instance": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "RequestExtension": {
    "Body": [
      {
        "Field": "Form.BankDetails.AccountNumber",
        "Reason": "SchemaValidation",
        "Description": "must match pattern \"^[0-9]{8,17}$\""
      }
    ]
  }
}
```

### Field not supported

Returned when a value is well-formed but not supported for the selected channel — for example a bank account whose IBAN does not support SEPA:

```json
{
  "Type": "InvalidMessage",
  "Action": "validate sepa support",
  "Detail": "invalid request",
  "Instance": "d0e1f2a3-b4c5-6789-0abc-def123456789",
  "RequestExtension": {
    "Body": [
      {
        "Field": "Form.BankDetails.AccountNumber",
        "Reason": "NotSupported",
        "Description": "IBAN does not support SEPA transfers"
      }
    ]
  }
}
```

### Self reference

Returned when the request points back at the caller in a way that is not allowed — for example withdrawing to your own deposit address:

```json
{
  "Type": "InvalidMessage",
  "Action": "workflow rest post bank to address",
  "Detail": "invalid request",
  "Instance": "e1f2a3b4-c5d6-7890-abcd-ef1234567890",
  "RequestExtension": {
    "Body": [
      {
        "Field": "DestinationAddress.Address",
        "Reason": "SelfReference",
        "Description": "can not use own address for withdrawal action"
      }
    ]
  }
}
```

### Resource not found

```json
{
  "Type": "ResourceNotFound",
  "Action": "fetch customer",
  "Detail": "customer not found",
  "Instance": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
  "RequestExtension": {
    "Body": [
      {
        "Field": "CustomerID",
        "Reason": "IntegrityValidation",
        "Description": "not found"
      }
    ]
  }
}
```

### Insufficient balance

```json
{
  "Type": "InsufficientBalance",
  "Action": "withdraw balance check",
  "Detail": "available balance less than requested amount",
  "Instance": "c3d4e5f6-a7b8-9012-cdef-123456789012"
}
```

### Unauthorized

Returned when the API key is missing, invalid, or expired, or when request signing fails:

```json
{
  "Type": "Unauthorized",
  "Action": "api authorizer",
  "Detail": "api key has expired",
  "Instance": "f6a7b8c9-d0e1-2345-f012-456789012345"
}
```

### Compliance denial

```json
{
  "Type": "Forbidden",
  "Action": "create customer account",
  "Detail": "Customer is in compliance review with the external provider",
  "Instance": "d4e5f6a7-b8c9-0123-def0-234567890123"
}
```

Channel policy denials may also include structured `DenyExtension` details:

```json
{
  "Type": "Forbidden",
  "Action": "evaluate channel policy",
  "Detail": "access denied",
  "Instance": "e5f6a7b8-c9d0-1234-ef01-345678901234",
  "DenyExtension": [
    {
      "Reason": "Compliance",
      "Description": "Channel policy denied use of this channel for the counterparty",
      "Principal": {
        "ID": "cust_123",
        "Type": "Customer",
        "Subtype": "Individual"
      },
      "Resource": {
        "ID": "00000000-0000-0000-0000-000000000123",
        "Type": "Channel"
      }
    }
  ]
}
```

Feature or currency restrictions use the same `DenyExtension` shape with `Reason: InsufficientPermissions`:

```json
{
  "Type": "Forbidden",
  "Action": "authorise client",
  "Detail": "user does not have permission to use this resource",
  "Instance": "a7b8c9d0-e1f2-3456-7890-abcdef012345",
  "DenyExtension": [
    {
      "Reason": "InsufficientPermissions",
      "Description": "feature Sell with currency USDC",
      "Principal": {
        "ID": "user_456",
        "Type": "User",
        "Subtype": "Business"
      },
      "Resource": {
        "ID": "00000000-0000-0000-0000-000000000456",
        "Type": "Channel"
      }
    }
  ]
}
```

## What to do next

The error envelope tells you **what** failed. This section describes **what to do next** for common denial and validation errors. Do not retry the same request blindly — resolve the underlying condition first.

Start with the top-level `Type` field, then inspect the nested reason if present:

| `Type`                | Where to look next               | What to do                                                   |
| --------------------- | -------------------------------- | ------------------------------------------------------------ |
| `Forbidden`           | `DenyExtension[].Reason`         | See the [DenyExtension](#denyextension) table above          |
| `InsufficientBalance` | — (no nested reason)             | [Insufficient balance](#insufficient-balance)                |
| `InvalidMessage`      | `RequestExtension.Body[].Reason` | See the [RequestExtension](#requestextension) table above    |
| `ResourceNotFound`    | `RequestExtension.Body[].Field`  | Confirm the referenced resource exists and the ID is correct |
| `Unauthorized`        | `Detail`                         | Check API key validity, expiry, and request signing          |

### Summary

| Error                                                                              | Suggested action                                                           |
| ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
| `Forbidden` + `DenyExtension` (`Compliance` / channel policy)                      | Fetch customer details and check verification statuses before retrying     |
| `Forbidden` + `DenyExtension` (`VerificationRequired`, `Principal.Type: Customer`) | Initiate or resume customer onboarding; check `OnboardingStatus`           |
| `Forbidden` + `DenyExtension` (`VerificationRequired`, `Principal.Type: User`)     | Complete business-profile verification (KYB/KYC)                           |
| `Forbidden` + `DenyExtension` (`InsufficientPermissions`)                          | Confirm the feature or currency is enabled for your business profile       |
| `Forbidden` + `DenyExtension` (`AccountStatus`)                                    | Contact your Noah representative — account is suspended or restricted      |
| `InsufficientBalance`                                                              | Fund the account or use `DelayedSell: true` on prepare (where applicable)  |
| `InvalidMessage` + `NotSupported` (form field)                                     | Select a different channel or payment method that supports the destination |

### Channel and compliance denials

When you receive `Forbidden` with `DenyExtension` where `Reason` is `Compliance`, the customer or payout destination failed a policy check — for example compliance review, a blocked destination country, or incomplete verification for the requested fiat option.

1. Call [GET `/customers/{CustomerID}`](/api-reference/customer-by-id) and inspect `Verifications`.
2. Check `Verifications.Status` (`Pending`, `Approved`, or `Declined`) and per-entity status in `EntityVerifications`.
3. If `ActionsRequired` is present, direct the customer back through [hosted onboarding](/api-reference/create-onboarding-session).
4. Subscribe to or consult [Customer webhooks](./webhooks/customer.md) for status changes instead of polling.

Do not retry the transaction until the relevant verification is `Approved` for the fiat option you need.

### Verification required

When `DenyExtension.Reason` is `VerificationRequired`, additional identity or account verification is required before the operation can proceed. Check `Principal.Type` to determine whose verification is missing:

**Customer principal** (`Principal.Type` is `Customer`): the end customer must complete verification.

1. Call [POST `/onboarding/{CustomerID}`](/api-reference/create-onboarding-session) to create or resume a hosted onboarding session.
2. Redirect the customer to the returned `HostedURL`.
3. Check `OnboardingStatus` and `MissingSteps` in the response to see what still needs to be submitted.
4. Wait for [Customer webhooks](./webhooks/customer.md) confirming `Approved` before retrying.

See [Hosted Onboarding](../recipes/onboarding/hosted-onboarding.md) for the full flow.

**User principal** (`Principal.Type` is `User`): the API-calling business user or profile must complete verification — for example business KYB is incomplete.

1. Review the `Description` field for context (for example `The current user KYC status does not allow this action`).
2. Complete your business onboarding and KYB through the Business Dashboard or with your Noah representative.

### Insufficient permissions

When `DenyExtension.Reason` is `InsufficientPermissions`, your business profile or API key is not authorized for the requested resource — for example a currency or feature named in `Description` (such as `feature Sell with currency USDC`).

1. Review the `Description` field in `DenyExtension` for the restricted feature or currency.
2. Confirm the capability is enabled for your business profile with your Noah representative.
3. If you need a different currency or payment route, use [GET `/channels/sell`](/api-reference/channels) to find an enabled alternative.

### Account status

When `DenyExtension.Reason` is `AccountStatus`, the account is suspended or otherwise restricted and cannot perform the requested action.

1. Review the `Description` field for context (for example `Account suspended`).
2. Contact your Noah representative to understand the restriction and the steps required to restore access.
3. Do not retry the request until the account status is resolved.

### Insufficient balance

When `Type` is `InsufficientBalance` (HTTP `402`), the account does not have enough crypto balance to cover the requested amount.

1. Check current balances with [GET `/balances`](/api-reference/balances).
2. Fund the account with additional crypto, or reduce the requested amount.
3. On sell prepare flows, set `DelayedSell: true` on [POST `/transactions/sell/prepare`](/api-reference/prepare-sell-transaction) to defer the balance check until the final sell request — useful when preparing a payout before the customer has deposited.

### Form field not supported

When `RequestExtension.Body` contains `Reason: NotSupported` on a form field, the value is valid but not supported for the selected channel — for example an IBAN that does not support SEPA.

1. Read the `Description` for the specific constraint.
2. Ask the customer for an alternative payment method or destination that the channel supports.
3. Use [GET `/channels/sell`](/api-reference/channels) or [GET `/channels/sell/countries`](/api-reference/countries) to find a channel that supports the destination.
4. If using a dynamic form, fetch a fresh schema with [GET `/channels/{ChannelID}/form`](/api-reference/dynamic-form) for the new channel before resubmitting.
