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

# Resource IDs

> Every resource has a readable, typed ID: a resource-type prefix plus a Base58 encoding of the underlying UUID. Foreign-key fields like withdrawalAccountId and quoteId reference these IDs across resources.

Every resource in v3 (`Quote`, `Trade`, `Deposit`, `Withdrawal`, `WithdrawalAccount`, `Account`, `FinancialAccount`, `PayoutAddress`, `PayoutQuote`, `Payout`, `AccountCapability`) carries an `id` that the server assigns at creation, plus an `object` field naming the resource type. IDs are **readable**: a lowercase 3-to-6-letter prefix identifies the resource type, followed by a Base58 encoding of the underlying 128-bit UUID. Two fields in v3 reference these IDs across resources: `quoteId` on a trade points at a `Quote.id`, and `withdrawalAccountId` on a withdrawal points at a `WithdrawalAccount.id`.

<Note>
  **v3 emits readable IDs on every response.** Bare UUIDs never appear in a v3
  response body; the readable ID is the public identifier. For backward
  compatibility, v3 **accepts** either a readable ID or a bare UUID v4 wherever
  an ID is expected on input (path parameters, query parameters, and
  request-body foreign keys); both decode to the same underlying record. New
  integrations should use the readable form returned by the API.
</Note>

## Format

`<prefix>_<base58(uuid)>`: a lowercase 3-to-6-letter type prefix, an underscore, and the Base58 encoding of the resource's 128-bit UUID.

| Resource | `object` | Prefix | Example |
| - | - | - | - |
| Quote | `quote` | `qte_` | `qte_3FfGK34vwMvVFDedyb2nkf` |
| Trade | `trade` | `tde_` | `tde_5W7guYdHT24JFnRQrZN9y8` |
| Deposit | `deposit` | `dpt_` | `dpt_EA9vbVngB76PmMibCiMNPb` |
| Withdrawal | `withdrawal` | `wtd_` | `wtd_6SATV6VSUdBTttHWBmCYjD` |
| Withdrawal account | `withdrawalAccount` | `wac_` | `wac_NDqQ9LmcUASpnHR6CTvdkk` |
| Account | `account` | `acc_` | `acc_BMY3UenpGBn8ZQ8hio4BgE` |
| Financial account | `financialAccount` | `finacc_` | `finacc_AmKC5dMVXUbiAW8VzKBfqz` |
| Payout address | `payoutAddress` | `pytadd_` | `pytadd_PbDQ126YUAbkNrNX6XbGL` |
| Payout quote | `payoutQuote` | `pytqte_` | `pytqte_5PKPvtstSbXf9BurnLJfLA` |
| Payout | `payout` | `pyt_` | `pyt_9P3Gs3GSn4hMdHQ22Evi5f` |
| Account capability | `accountCapability` | `cap_` | `cap_Ji86R1HUP9uUTtp9kdkqBw` |
| User | — | `usr_` | `usr_7m4VsfRw4pGrS76WYj5tnx` |

`Pair` and `Balance` have no resource UUID. A pair is identified by its `(buyCurrency, sellCurrency)` combination, and a balance by `currency`, not by an `id` field.

* The Base58 alphabet excludes `0`, `O`, `I`, and `l` to avoid visual ambiguity: `123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz`.
* The encoded body is at most 22 characters and is **not a fixed length**: 21–22 characters for a typical random UUID, shorter only for near-zero values (the nil UUID encodes to 16 characters).
* Regex pre-filter: `^[a-z]{3,6}_[1-9A-HJ-NP-Za-km-z]{1,22}$`. This is a loose shape check; the authoritative validation is a successful decode back to exactly 16 bytes. A value that matches the regex but doesn't decode to 16 bytes is rejected the same as a malformed ID. The payments operations are stricter: their specification pins each ID to its own prefix (for example `pyt_` or `acc_`) and a body of 21 or 22 characters, so a short body that passes the regex fails validation there. Use IDs exactly as the API returned them.
* Base58 is case-sensitive. There is no case normalization: a mis-cased but otherwise well-formed ID decodes to a different value (or fails to decode) and is rejected.

Encoding is a pure, stateless, reversible function of the UUID: the same UUID always produces the same readable ID, and two different UUIDs never collide. There's no database column or backfill involved; the readable ID is derived from the UUID on every response.

Any ID that fails to decode, or whose prefix names a different resource type than the field expects, is rejected at the validation layer with `400` <span class="ofx-enum">VALIDATION\_QUERY\_FAILED</span> (query and body) or `400` <span class="ofx-enum">VALIDATION\_PATH\_FAILED</span> (path parameters).

## Stability

* IDs are **permanent**. Once issued, the underlying UUID never changes and never gets reused, so its readable encoding doesn't either.
* Safe to store as foreign keys in your database. Index the readable ID, the underlying UUID, or both, depending on what your queries need.
* Safe to log in audit trails. IDs carry no PII: the prefix reveals only the resource type, and the body is an opaque, lossless encoding of the UUID.
* Safe to share in support tickets and idempotency-key collision reports.

The stability contract is the same contract that makes idempotency keys useful: if you persist a resource ID at write time and the network drops the response, that ID will still resolve to the same record on retry.

## Cross-resource references

Some fields are foreign keys pointing at IDs on other resources. The table below lists every such reference in v3:

| Field | On resource | Points to | Notes |
| - | - | - | - |
| `quoteId` | `TradeCreateRequest`, `Trade` | `Quote.id` | Required when executing a trade. Echoed back on the resulting `Trade`. |
| `withdrawalAccountId` | `WithdrawalCreateRequest`, `Withdrawal` | `WithdrawalAccount.id` | The verified withdrawal account. Fiat or stablecoin is inferred from the account. |
| `actorId` | `Withdrawal` | User (`usr_` prefix) | The actor that initiated the withdrawal. Masked to a fixed placeholder for internal OpenFX operators. Not independently fetchable; v3 has no `GET` for user records. |
| `creatorId` | `WithdrawalAccount` | User (`usr_` prefix) | The user that created the withdrawal account. `null` only for legacy rows. Not independently fetchable. |
| `financialAccountId` | `Payout`, `PayoutQuote` | `FinancialAccount.id` | The balance a payout is funded from. |
| `receiver.accountId` | `Payout` | `Account.id` | Derived from `receiver.payoutAddressId`: the account that address belongs to. |
| `receiver.payoutAddressId` | `Payout` | `PayoutAddress.id` | The payout address the funds are credited to. |
| `accountId` | `PayoutAddress` | `Account.id` | The account the payout address belongs to. |

Anything else that looks like an ID (a transaction hash, a currency code) is not a foreign key into another v3 resource.

## ID vs Idempotency-Key

Two ID-shaped fields in v3 serve different purposes:

| | Resource ID | Idempotency-Key |
| - | - | - |
| Generated by | Server | Client |
| Lifetime | Permanent | TTL (30 min / 24 h / 7 days, per endpoint) |
| Format | `<prefix>_<base58>` (e.g. `wac_NDqQ9LmcUASpnHR6CTvdkk`) | Any string matching `^[a-zA-Z0-9_-]{1,255}$` |
| Where it lives | Response body `id`, foreign-key fields | `Idempotency-Key` request header |
| Purpose | Identify a record forever | De-duplicate a single write attempt |
| Safe to log | Yes | Yes (but don't embed PII in them) |

The two are complementary: you generate an idempotency key before the request, the server returns a resource ID in the response, and you persist both alongside each other. The idempotency key lets you safely retry the write; the resource ID lets you look up the result for the rest of time. See [Idempotency → Crash recovery](/v3/idempotency#crash-recovery) for the persist-both pattern.

## Common mistakes

* **Treating IDs as transient.** They aren't. Persist them.
* **Parsing or decoding the readable ID yourself.** The prefix tells you the resource type; don't try to derive anything else from the body. Treat the body as an opaque string, even though it happens to be reversible server-side.
* **Reading the `id` from your own DB row instead of the API response on a write.** If you assigned an identifier locally and sent it as the request body, that's not the resource ID. The server generates its own. Always trust the response, not your draft.
* **Confusing `quoteId` with `Idempotency-Key` on the trade execution call.** `quoteId` goes in the JSON body, `Idempotency-Key` goes in the header. They're unrelated.
* **Storing only the readable ID and losing the ability to index efficiently.** Both the readable ID and its underlying UUID identify the same record. Store whichever shape fits your schema, or both.
* **Assuming a hardcoded body length.** The encoded body is at most 22 characters but isn't fixed-width. Validate by prefix and successful decode, not by string length.

## What's next

<CardGroup cols={2}>
  <Card title="Idempotency" icon="https://public-assets.openfx.com/docs/icons/idempotency.svg" href="/v3/idempotency">
    Client-generated keys vs server-generated IDs.
  </Card>

  <Card title="Pagination" icon="https://public-assets.openfx.com/docs/icons/pagination.svg" href="/v3/pagination">
    Opaque cursors, distinct from resource IDs. Pass them back verbatim.
  </Card>

  <Card title="Errors" icon="https://public-assets.openfx.com/docs/icons/errors.svg" href="/v3/errors">
    When you'll see `*_NOT_FOUND` and how to handle it.
  </Card>
</CardGroup>
