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:

FieldTypeNotes
idstring (uuid)
instanceIdstring (uuid)
intentType"onramp" | "offramp"
flowVersionintegerPin on at least the version your code was written against.
statussee status enum
scheduledForstring | omittedISO 8601. Present only while an offramp is scheduled. It is the earliest time payout processing can start, not a completion guarantee.
statusReasonstring | nullHuman-readable status context.
failureCodeenum | nullMachine-readable failure class for supported failures.
providerFailureCodestring | nullRaw provider rejection code, when available.
originalFailureCodeenum | nullInitial failure class when a later failed event changes failureCode.
originalProviderFailureCodestring | nullRaw provider code for the initial failure.
clientReferencestring | nullEchoed from your body.
metadataobject | null
expiresAtstring | nullISO 8601.
createdAtstringISO 8601.
updatedAtstring
pixPaymentobject | nullPIX deposit info while an onramp awaits payment; null on offramps and once the onramp is past awaiting_fiat_payment.
depositInstructionsnullAlways null in the current /v2 response.
partyobject | nullThe 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:

FieldTypeNotes
brcodestringPIX BR Code — present on all payment rails.
txidstringTransaction ID.
amountCentsinteger
expiresAtstring | nullISO 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

HeaderRequiredNotes
X-API-KeyYes
Content-TypeYesapplication/json
idempotency-keyRecommendedUUID (format-validated). One key per user intent; reuse across retries.

Request body

FieldTypeRequiredDescription
amountCentsintegerYesBRL amount in cents. Must be ≥ 500 (R$5.00 = 5 BRS minimum).
destinationWalletstringYesSolana address, or a whitelisted Ethereum address (1–255 chars).
chainIdstringYes"solana", "ethereum", "base", or "polygon".
clientReferencestringNoUp 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 for destinationWallet), evm_destination_wallet_rate_limit_exceeded (daily limit below amountCents), 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 a clientReference that 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

HeaderRequiredNotes
X-API-KeyYes
Content-TypeYesapplication/json
idempotency-keyRecommendedUUID (format-validated).

Request body

FieldTypeRequiredDescription
amountCentsintegerYesBRL payout amount in cents. Must be ≥ 500 (R$5.00 = 5 BRS minimum).
destinationAddressstringYesPIX 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.
chainIdstringYes"solana", "ethereum", "base", or "polygon".
clientReferencestringNoUp 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 a clientReference that already exists in this environment (Intent with clientReference 'payout_xyz_42' already exists). Offramp replays a retry only when no clientReference is sent; with one, every retry gets this 409, so page through GET /v2/intents?intentType=offramp and match clientReference (no filter exists for it).

GET /v2/intents/:id

Fetch a single intent by id.

Path parameters

ParamTypeRequired
idstring (uuid)Yes

Headers

HeaderRequired
X-API-KeyYes

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

HeaderRequired
X-API-KeyYes

Query parameters

ParameterTypeDefaultDescription
intentTypestring"all""onramp", "offramp", or "all".
statusstring"all"Any intent status (see enum above) or "all".
limitnumber501–100.
offsetnumber0Zero-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

FieldTypeRequiredDescription
tokenHolderstringYesThe 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 unsigned user_burn transaction appears at metadata.userBurnTransaction on 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. from is the only address that may send the call. expiresAt is earlier than the intent's own expiresAt, 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 answering preparing. Create a new offramp intent only if no burn was broadcast; a broadcast burn's hash is still accepted by approve-burn until the intent expires. If a fresh intent behaves the same way, stop and contact Nora with the intent id.

Errors

  • 400 — the intent is not awaiting_burn_approval, the address family does not match the intent's chain, amountCents is below the minimum, or (Ethereum) a different tokenHolder while an authorization is still live. On Solana a different tokenHolder while a grant is live is not rejected: the call answers preparing and the first holder stays bound; compare metadata.burnTokenHolder with 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.tokenHolder carries the address), or Intent 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

ParamTypeRequired
idstring (uuid)Yes

Headers

HeaderRequiredNotes
X-API-KeyYes
Content-TypeYesapplication/json

No separate idempotency-key header. One proof per intent: after a successful call the intent is burn_approved and a repeat answers 400.

Request body

FieldTypeRequiredDescription
txSignaturestringSolanaBase58 signature of the broadcast user_burn transaction.
txHashstringEthereum0x plus 64 hex characters: the hash of the transaction that carried the burn.
tokenHolderstringYesThe 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 longer awaiting_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.

On this page