Intents
Create onramp / offramp intents, fetch and list them, prepare and approve the burn for offramps.
Intents are the state-tracked transactions between your end-users' fiat
and on-chain balances. Two flavours: onramp (fiat → crypto) and
offramp (crypto → fiat). Offramps additionally require an on-chain
burn from the token holder's wallet, prepared with prepare-burn and
reported back to Nora via approve-burn.
chainId accepts "solana", "ethereum", or "base". Base is live in both environments (sandbox = Base Sepolia, production = Base mainnet). It also accepts "polygon", live in both environments (sandbox = Polygon Amoy, production = Polygon mainnet).
Error envelope
All intents endpoints return errors in the intents-family shape:
{
"code": "BAD_REQUEST",
"message": "evm_destination_wallet_not_authorized",
"details": { }
}code and message are present on every non-2xx response except
structural validation failures, which come back as
{ "data": …, "error": [{ "code", "path", "message" }], "success": false }
(for example amountCents below the minimum). details is optional and
endpoint-specific. See
Error handling.
The intent response shape
Create (200/201), fetch (200), and the items inside list
(200) all return the same intent object:
| Field | Type | Notes |
|---|---|---|
id | string (uuid) | |
instanceId | string (uuid) | |
intentType | "onramp" | "offramp" | |
flowVersion | integer | Pin on at least the version your code was written against. |
status | see status enum | |
scheduledFor | string | omitted | ISO 8601. Present only while an offramp is scheduled. It is the earliest time payout processing can start, not a completion guarantee. |
statusReason | string | null | Human-readable status context. |
failureCode | enum | null | Machine-readable failure class for supported failures. |
providerFailureCode | string | null | Raw provider rejection code, when available. |
originalFailureCode | enum | null | Initial failure class when a later failed event changes failureCode. |
originalProviderFailureCode | string | null | Raw provider code for the initial failure. |
clientReference | string | null | Echoed from your body. |
metadata | object | null | |
expiresAt | string | null | ISO 8601. |
createdAt | string | ISO 8601. |
updatedAt | string | |
pixPayment | object | null | PIX deposit info while an onramp awaits payment; null on offramps and once the onramp is past awaiting_fiat_payment. |
depositInstructions | null | Always null in the current /v2 response. |
party | object | null | The party behind the intent: displayName, documentMasked, and solanaWallet (the party's bound Solana wallet, null when it has none). |
chainId | "solana" | "ethereum" | "base" | "polygon" | The chain the intent settles on. |
pixPayment when present:
| Field | Type | Notes |
|---|---|---|
brcode | string | PIX BR Code — present on all payment rails. |
txid | string | Transaction ID. |
amountCents | integer | |
expiresAt | string | null | ISO 8601; null for non-expiring codes. |
Status enum
created, requires_compliance, awaiting_fiat_payment,
fiat_received, minting, awaiting_onchain_transfer,
awaiting_burn_approval, burn_approved, onchain_received,
burning, scheduled, paying_out, completed, expired, canceled, failed,
refunding, refunded.
POST /v2/intents/onramp
Create an onramp intent. The response includes pixPayment to surface to
your user.
Headers
| Header | Required | Notes |
|---|---|---|
X-API-Key | Yes | |
Content-Type | Yes | application/json |
idempotency-key | Recommended | UUID (format-validated). One key per user intent; reuse across retries. |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
amountCents | integer | Yes | BRL amount in cents. Must be ≥ 500 (R$5.00 = 5 BRS minimum). |
destinationWallet | string | Yes | Solana address, or a whitelisted Ethereum address (1–255 chars). |
chainId | string | Yes | "solana", "ethereum", "base", or "polygon". |
clientReference | string | No | Up to 255 chars. Echoed on the intent. Unique per environment, forever, across both intent types; defaults to the idempotency-key. |
Response 200 / 201
The intent response shape above.
Example
curl -X POST https://sandbox.api.nora.finance/v2/intents/onramp \
-H "X-API-Key: $NORA_API_KEY" \
-H "Content-Type: application/json" \
-H "idempotency-key: $(uuidgen)" \
-d '{
"amountCents": 10000,
"destinationWallet": "So11111111111111111111111111111111111111112",
"chainId": "solana",
"clientReference": "ord_123"
}'Errors
400— body validation (negative amount, missing field, address format), or on Ethereum the on-chain check of your whitelister:evm_destination_wallet_not_authorized(no authorization fordestinationWallet),evm_destination_wallet_rate_limit_exceeded(daily limit belowamountCents),evm_mint_amount_below_minimum/evm_mint_amount_above_maximum(outside the BRS token's per-mint bounds).409— idempotency-key collision with a different body, or aclientReferencethat already exists in this environment (Intent with clientReference 'ord_123' already exists); references are unique forever, expired intents included.
POST /v2/intents/offramp
Create an offramp intent. Once created, call prepare-burn — Nora
issues an attested burn authorization for the token holder — then have
the holder burn on-chain and report the proof via approve-burn
(txSignature on Solana, txHash on Ethereum). See
Burn signing.
Headers
| Header | Required | Notes |
|---|---|---|
X-API-Key | Yes | |
Content-Type | Yes | application/json |
idempotency-key | Recommended | UUID (format-validated). |
Request body
| Field | Type | Required | Description |
|---|---|---|---|
amountCents | integer | Yes | BRL payout amount in cents. Must be ≥ 500 (R$5.00 = 5 BRS minimum). |
destinationAddress | string | Yes | PIX key on your organization's Pix-key list: your own CNPJ, an email, a +55 phone or an EVP key. CPF and other companies' CNPJs are rejected. |
chainId | string | Yes | "solana", "ethereum", "base", or "polygon". |
clientReference | string | No | Up to 255 chars. Unique per environment, forever, across both intent types; defaults to the idempotency-key. |
Response 200 / 201
The intent response shape above. pixPayment is null on offramps and
depositInstructions is always null.
Example
curl -X POST https://sandbox.api.nora.finance/v2/intents/offramp \
-H "X-API-Key: $NORA_API_KEY" \
-H "Content-Type: application/json" \
-H "idempotency-key: $(uuidgen)" \
-d '{
"amountCents": 10000,
"destinationAddress": "user@example.com",
"chainId": "solana",
"clientReference": "payout_xyz_42"
}'Errors
400— body validation; a key not on your Pix-key list (The Pix key attempting to receive the offramp is not whitelisted by the organization.); a CNPJ key that is not your own (destinationAddress must equal the organization CNPJ for CNPJ-typed Pix keys.).409— idempotency-key collision, or aclientReferencethat already exists in this environment (Intent with clientReference 'payout_xyz_42' already exists). Offramp replays a retry only when noclientReferenceis sent; with one, every retry gets this 409, so page throughGET /v2/intents?intentType=offrampand matchclientReference(no filter exists for it).
GET /v2/intents/:id
Fetch a single intent by id.
Path parameters
| Param | Type | Required |
|---|---|---|
id | string (uuid) | Yes |
Headers
| Header | Required |
|---|---|
X-API-Key | Yes |
Response 200
The intent response shape above.
Errors
404— no intent with that id in this org/instance.
GET /v2/intents
List intents — filterable and paginated with limit / offset.
Headers
| Header | Required |
|---|---|
X-API-Key | Yes |
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
intentType | string | "all" | "onramp", "offramp", or "all". |
status | string | "all" | Any intent status (see enum above) or "all". |
limit | number | 50 | 1–100. |
offset | number | 0 | Zero-based. |
Response 200
{
"intents": [
{ "id": "…", "status": "…", "…": "…" }
],
"total": 42,
"limit": 50,
"offset": 0
}Pagination is offset-based (not cursor-based). The key is intents
(plural), not items. total is the full match count for the
filter.
POST /v2/intents/:id/prepare-burn
Issue the burn authorization for an offramp intent in
awaiting_burn_approval.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
tokenHolder | string | Yes | The wallet that holds the BRS and will burn. Base58 on Solana, 0x on Ethereum, 1–255 chars; the wrong family is a 400. |
Response 200
- Solana: always
{ "status": "preparing" }. The unsigneduser_burntransaction appears atmetadata.userBurnTransactionon the intent. - Ethereum:
{ "status": "preparing" }until the authorization is issued, then{ "status": "ready", "burn": { chainRef, from, to, data, amountMinor, expiresAt } }on every later call while it is live.fromis the only address that may send the call.expiresAtis earlier than the intent's ownexpiresAt, so the holder can still burn and you can still approve inside the intent's window. A lapsed authorization is not replaced: by then the intent is too close to its own expiry for a new one, so the call keeps answeringpreparing. Create a new offramp intent only if no burn was broadcast; a broadcast burn's hash is still accepted byapprove-burnuntil the intent expires. If a fresh intent behaves the same way, stop and contact Nora with the intent id.
Errors
400— the intent is notawaiting_burn_approval, the address family does not match the intent's chain,amountCentsis below the minimum, or (Ethereum) a differenttokenHolderwhile an authorization is still live. On Solana a differenttokenHolderwhile a grant is live is not rejected: the call answerspreparingand the first holder stays bound; comparemetadata.burnTokenHolderwith the holder you asked for before handing out the staged transaction.400(Solana only) —burn_wallet_not_authorized(the holder is not a registered mint/burn wallet of your organization),token_holder_not_authorized(not whitelisted on-chain;details.tokenHoldercarries the address), orIntent expires too soon to prepare a burn. Create a new intent and prepare the burn on it.404— intent not found.
POST /v2/intents/:id/approve-burn
Submit the proof of the on-chain burn for an offramp intent.
Path parameters
| Param | Type | Required |
|---|---|---|
id | string (uuid) | Yes |
Headers
| Header | Required | Notes |
|---|---|---|
X-API-Key | Yes | |
Content-Type | Yes | application/json |
No separate
idempotency-keyheader. One proof per intent: after a successful call the intent isburn_approvedand a repeat answers400.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
txSignature | string | Solana | Base58 signature of the broadcast user_burn transaction. |
txHash | string | Ethereum | 0x plus 64 hex characters: the hash of the transaction that carried the burn. |
tokenHolder | string | Yes | The same address passed to prepare-burn. Base58 on Solana (the wallet owner, not the ATA), 0x on Ethereum. |
Use the proof field of the intent's chain. The other chain's field name is
tolerated and means the same field, but the value must be well-formed for
the intent's chain (base58 signature on Solana, 0x 32-byte hash on
Ethereum); otherwise 400. Sending both is allowed only when they are
identical.
Response 200
{ "status": "burn_approved" }Single field — no intent echo. Re-fetch GET /v2/intents/:id to
observe subsequent transitions.
Errors
400— body validation, a proof that is not well formed for the intent's chain, or the intent is no longerawaiting_burn_approval.404— intent not found.
On both chains the proof is accepted after a format check and verified in
the background; a proof that does not land or does not match fails the
intent later with a statusReason naming the cause — see
Burn signing.