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

# Error Codes and Handling

> Stable error codes returned by the Trench Partner API, grouped by auth, trading, governance, and validation, with the meaning of each.

The Trench Partner API returns standardized error responses so you can handle failures consistently. Every error is JSON and carries a stable `error` code, which is what you should match on. The `message` field is written for humans and may change without notice.

## Error Response Format

```json theme={null}
{
  "error": "insufficient_sol",
  "message": "You do not have enough SOL for this transaction. Add SOL to your wallet and try again.",
  "retryable": false,
  "requiredLamports": "105000000",
  "availableLamports": "98000000"
}
```

| Field       | Always present | Meaning                                            |
| ----------- | -------------- | -------------------------------------------------- |
| `error`     | Yes            | Stable machine-readable code                       |
| `message`   | Usually        | Human-readable text, safe to show a user           |
| `action`    | No             | Suggested next step, on codes where one exists     |
| `retryable` | No             | Whether an identical retry could plausibly succeed |

Code-specific fields are additive, so we recommend handling their absence rather than assuming they are present.

## Auth and Access

| Status | Error                      | Meaning                                                                                  |
| ------ | -------------------------- | ---------------------------------------------------------------------------------------- |
| 401    | `invalid_token`            | Expired, revoked, unknown, or the client was suspended                                   |
| 401    | `invalid_client`           | Bad client credentials on `/oauth/token` or `/oauth/revoke`                              |
| 403    | `insufficient_scope`       | Your token does not carry the required scope                                             |
| 403    | `delegation_missing`       | User has not enabled 1-click trading                                                     |
| 403    | `trade_cap_exceeded`       | Over your per-trade limit. Adds `maxTradeLamports`                                       |
| 403    | `daily_cap_exceeded`       | Over the daily volume for this user. Adds `dailyVolumeCapLamports`, `spentTodayLamports` |
| 400    | `invalid_grant`            | Bad, expired, or replayed authorization code or refresh token                            |
| 400    | `invalid_request`          | A field required by the grant type is missing                                            |
| 400    | `unsupported_grant_type`   | Not `authorization_code` or `refresh_token`                                              |
| 400    | `invalid_scope`            | Scope string is empty or contains an unknown scope                                       |
| 400    | `scope_not_allowed`        | Real scope, but not approved for your client                                             |
| 400    | `invalid_redirect_uri`     | Not an exact match, or plain HTTP on a live client                                       |
| 400    | `unknown_client`           | No such client, or it is suspended                                                       |
| 429    | `rate_limit_exceeded`      | Over your tier's requests per minute. Honour `retry-after`                               |
| 429    | `too_many_failed_attempts` | Too many failed API key authentications, locked out for five minutes                     |

<Warning>
  Handle `401 invalid_token` by refreshing once and then re-authorizing if that fails. Retrying in a loop or refreshing concurrently for the same user can leave you replaying a rotated-out refresh token, which revokes the entire connection.
</Warning>

<Warning>
  Never retry a `401 invalid_client`. Twenty failed authentications against one `client_id` within five minutes lock the app out entirely, so a stale API key in a retry loop will take your working traffic down with it. Treat it as a configuration problem to fix rather than a transient failure.
</Warning>

## Trading

| Status | Error                        | Meaning                                                                    |
| ------ | ---------------------------- | -------------------------------------------------------------------------- |
| 400    | `invalid_partner_fee`        | `partnerFeeBps` is not an integer between 0 and 9999                       |
| 400    | `partner_fee_not_configured` | Your tier cannot charge fees, or no fee wallet is registered               |
| 409    | `migration_pending`          | Curve complete, migration still finalizing. Retryable                      |
| 409    | `pool_not_open`              | Raydium pool is not open for trading yet. Retryable                        |
| 422    | `insufficient_sol`           | Adds `requiredLamports`, `availableLamports`                               |
| 422    | `insufficient_tokens`        | Adds `requiredTokens`, `availableTokens`                                   |
| 422    | `insufficient_liquidity`     | The curve cannot fill an order this large                                  |
| 422    | `curve_complete`             | The curve completed while the trade was in flight                          |
| 422    | `missing_sell_token_account` | User holds none of this token                                              |
| 422    | `slippage_exceeded`          | Price moved past the bound, whether you set it or `slippageBps` derived it |
| 422    | `buy_limit_exceeded`         | Would exceed the per-wallet token cap                                      |
| 422    | `same_slot_trading_blocked`  | Trading opens on the next block                                            |
| 422    | `math_overflow`              | Requested amount is too large                                              |

<Note>
  The three migration codes describe one sequence. A token leaves the bonding curve, spends a short period in `migration_pending` while the Raydium pool is created, and may briefly return `pool_not_open` before trading resumes. All three clear on their own, so retrying after a delay is the right response.

  You do not need to route migrated tokens elsewhere yourself. The [trade endpoints](/api-reference/trades) resolve the venue per request and execute against the pool once migration completes.
</Note>

## Governance

| Status | Error                             | Meaning                                          |
| ------ | --------------------------------- | ------------------------------------------------ |
| 404    | `governance_not_found`            | Token has no governance account                  |
| 422    | `already_voted`                   | User already has an active vote on this proposal |
| 422    | `invalid_vote_amount`             | Below `minLockAmount`, or exceeds balance        |
| 422    | `missing_vote_token_account`      | User holds none of the token                     |
| 422    | `vote_not_active`                 | The vote was cancelled                           |
| 422    | `vote_already_withdrawn`          | Locked tokens already reclaimed                  |
| 422    | `vote_receipt_not_found`          | No vote from this wallet on this proposal        |
| 422    | `proposal_closed`                 | Voting has ended                                 |
| 422    | `proposal_still_open`             | Cannot settle until voting closes                |
| 422    | `proposal_already_settled`        | Already settled                                  |
| 422    | `proposal_not_settled`            | Withdrawal requires settlement first             |
| 422    | `proposal_not_found`              | Unknown proposal                                 |
| 422    | `invalid_proposal`                | Proposal account could not be read               |
| 422    | `active_proposal_exists`          | One proposal at a time per governance            |
| 422    | `insufficient_proposal_bond`      | User cannot cover the bond                       |
| 422    | `missing_proposal_token_account`  | User must hold the token to propose              |
| 422    | `no_proposal_bond`                | Nothing to withdraw                              |
| 422    | `proposal_bond_already_withdrawn` | Bond already reclaimed                           |
| 422    | `proposal_window_closing`         | Not enough time left before metadata locks       |

### Metadata and Mutability

| Status | Error                              | Meaning                                                |
| ------ | ---------------------------------- | ------------------------------------------------------ |
| 422    | `governance_finalized`             | Metadata is locked forever                             |
| 422    | `metadata_not_mutable`             | Metadata can no longer be changed                      |
| 422    | `metadata_still_mutable`           | The window has not expired yet                         |
| 422    | `content_field_locked`             | A field in the change set is permanently locked        |
| 422    | `no_metadata_changes`              | Nothing differs from current on-chain state            |
| 422    | `stale_content_revision`           | Metadata changed while the proposal was being authored |
| 422    | `mutability_expired`               | The mutable window has closed                          |
| 422    | `mutability_already_extended`      | Already extended once                                  |
| 422    | `mutability_extension_exceeds_cap` | Requested extension is too long                        |
| 422    | `invalid_vote_duration`            | Vote duration is not a valid value                     |
| 422    | `vote_duration_exceeds_window`     | Longer than the remaining mutable window               |

<Warning>
  A `stale_content_revision` response means another proposal landed while your user was editing. Re-read [`GET /partner/v1/governance/{mint}`](/api-reference/governance-read), rebase the change set onto the new `contentRevision`, and ask the user to confirm before resubmitting.
</Warning>

## Validation

| Status | Error                | Meaning                                                                                                                                                 |
| ------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `invalid_input`      | Request body failed schema validation                                                                                                                   |
| 400    | `invalid_multipart`  | The uploaded form could not be parsed                                                                                                                   |
| 400    | `invalid_image_type` | Unsupported image type. Use PNG, JPG, WebP, or GIF                                                                                                      |
| 400    | `invalid_name`       | Name cannot be empty                                                                                                                                    |
| 400    | `invalid_symbol`     | Symbol cannot be empty                                                                                                                                  |
| 400    | `name_too_long`      | Use 32 bytes or fewer                                                                                                                                   |
| 400    | `symbol_too_long`    | Use 10 bytes or fewer                                                                                                                                   |
| 422    | `invalid_wallet`     | The user's wallet address is invalid                                                                                                                    |
| 429    | varies               | [Rate limited](/concepts/rate-limits). Tier limits return `rate_limit_exceeded`, while per-endpoint limits return standard headers with no `error` code |

### Link Fields on update-content

The `website`, `twitter`, and `telegram` fields on [`proposals/update-content`](/api-reference/governance-propose) are validated separately from the rest of the body, and they are currently the one place where the error contract does not hold.

| Condition                                | Intended code                                               |
| ---------------------------------------- | ----------------------------------------------------------- |
| Malformed URL, wrong host, or bad handle | `invalid_website`, `invalid_twitter`, `invalid_telegram`    |
| Longer than 300 characters               | `website_too_long`, `twitter_too_long`, `telegram_too_long` |

<Warning>
  All six of these currently return `500` with a generic `internal` body rather than a `400`. They are thrown as genuine validation errors but are missing from the server's error catalog, so they fall through to the default status.

  This matters because the usual handling strategy treats a `500` as transient and retries it. These failures are permanent, and the same input will fail identically every time, so we recommend validating these three fields on your own side before submitting.
</Warning>

Normalization is strict. The `website` field must resolve to `https://` with a dotted hostname, `twitter` must reduce to a handle matching `[A-Za-z0-9_]{1,15}`, and `telegram` to `[A-Za-z0-9_]{5,32}`. Bare handles, `@handle`, and full URLs are all accepted as input and normalized to a canonical URL.

## Infrastructure

These errors are transient, and retrying with backoff is appropriate.

| Status | Error                | Meaning                                                     |
| ------ | -------------------- | ----------------------------------------------------------- |
| 502    | `rpc_unavailable`    | Solana network or landing service is unavailable            |
| 502    | `privy_sign_failed`  | The wallet signing service failed                           |
| 502    | `pinata_unavailable` | Image upload service is unavailable                         |
| 500    | `internal`           | Something went wrong on the Trench side                     |
| 422    | `tx_expired`         | Transaction expired before landing, so rebuild and resubmit |
| 422    | `tx_failed`          | Transaction failed on-chain                                 |
| 422    | `tx_too_large`       | Too large, so retry with fewer fields                       |

### Oracle

The SOL price oracle gates operations that price in USD, and all four of these errors are transient.

| Error                        | Meaning                                         |
| ---------------------------- | ----------------------------------------------- |
| `oracle_stale`               | Price feed is out of date                       |
| `oracle_unavailable`         | Feed could not be reached                       |
| `oracle_confidence_too_wide` | Confidence interval is too wide to price safely |
| `oracle_price_out_of_range`  | Price is outside the allowed band               |

## Error Handling in Code

The most useful way to group these is not by HTTP status but by what you should do next.

```javascript theme={null}
const REAUTH = new Set(["invalid_token", "invalid_grant"]);
const RECONNECT = new Set(["delegation_missing"]);
const MIGRATING = new Set(["migration_pending", "pool_not_open"]);
const TRANSIENT = new Set([
  "rpc_unavailable", "privy_sign_failed", "pinata_unavailable", "internal",
  "oracle_stale", "oracle_unavailable",
  "oracle_confidence_too_wide", "oracle_price_out_of_range",
]);

function handle(err) {
  if (REAUTH.has(err.error)) return refreshThenRetryOnce();
  if (RECONNECT.has(err.error)) return promptReconnect();
  if (MIGRATING.has(err.error)) return retryAfter({ seconds: 5, maxAttempts: 6 });
  if (TRANSIENT.has(err.error)) return retryWithBackoff({ maxAttempts: 4 });

  // Everything else is a real, user-visible outcome.
  return showMessage(err.message);
}
```

## Retry Logic

1. **429 (Rate Limited)**: wait for the interval in `retry-after`, then retry. Nothing was submitted on-chain.
2. **409 (`migration_pending`, `pool_not_open`)**: the token is between venues. Wait a few seconds and retry, since nothing was submitted on-chain.
3. **500/502 on reads**: retry with exponential backoff, up to a fixed attempt ceiling.
4. **400/401/403/404 and most 422s**: do not retry, since the request needs to change first.
5. **Any write endpoint**: see the caution below before retrying at all.

<Warning>
  We recommend giving the transient branch a hard attempt ceiling, because a `500` is not always genuinely transient, as the link-field case above shows. Without a ceiling, a single malformed `twitter` value can become an unbounded retry loop against a permanent failure.

  Write endpoints should not be placed on an automatic retry path at all. The API does not support idempotency keys, so a resent request can execute twice. See [Trades](/api-reference/trades) for the reconciliation pattern.
</Warning>

<Note>
  A `429` and a `403 daily_cap_exceeded` both indicate that you should slow down, but rate limits clear within the minute whereas volume caps reset at 00:00 UTC. We recommend handling them separately.
</Note>

<Note>
  Trench emits a broader error catalog than this page covers. Codes tied to token launches, creator rewards, referrals, and trader cashback only arise on first-party surfaces and are unreachable through the Partner API. Everything reachable with a partner access token is listed above.
</Note>
