Docs

REST API & API keys

Self-host the Noir API gateway: API keys, scopes, rate limits, endpoints, non-custodial transactions and errors.

The Noir API is a small, self-hostable gateway in front of the contracts, for apps that aren’t written in JavaScript or that want API keys, rate limits and managed webhooks. It is non-custodial: it never holds a secret key. Writes are prepared by the API, signed by your user’s wallet or your agent key, then submitted.

Run it

bash
cd sdk/noir-api
npm run setup && npm run build
npm start          # PORT=8787, Noir Testnet by default

Configuration is environment variables (.env.example lists them): CORS origins, default rate limit, webhook storage, and your own RPC or contract IDs.

API keys

bash
npm run keygen -- my-shop --scopes read,write,webhooks --rate 300
# Created key "my-shop" (id k3j9x0ab, scopes read,write,webhooks) in ./keys.json.
#   noir_sk_k3j9x0ab_4yQ…   ← shown once; only its SHA-256 hash is stored

Send it as Authorization: Bearer noir_sk_… (or X-API-Key). Keys carry scopes:

ScopeAllows
readNetwork, devices, policies, balances, payment checks, events
writetransactions/prepare and transactions/submit
webhooksCreate, list and delete webhook subscriptions

Each key has a token-bucket rate limit (default 120/min). Responses carry X-RateLimit-Limit and X-RateLimit-Remaining; over the limit you get 429 with Retry-After. Revoke a key by setting "disabled": true on its record.

Endpoints

Amounts are decimal strings in the asset’s smallest unit (1 XLM = "10000000"). Floats are rejected.

MethodPathScopeReturns
GET/v1/health—{ ok: true }
GET/v1/networkreadRPC URL, passphrase, contract IDs
GET/v1/devices/:hashread{ deviceHash, owner, policy, escrowBalance }
GET/v1/devices/:hash/policyread{ agent, maxAmount, asset, expiresAt }
GET/v1/wallets/:address/devicesread{ devices: [hash…] }
GET/v1/merchants/:address/pendingread{ pending }
POST/v1/payments/checkread{ ok } or { ok: false, reason }
GET/v1/events?fromLedger=&cursor=&limit=read{ events, cursor, latestLedger }
POST/v1/transactions/preparewrite{ xdr, networkPassphrase, operation }
POST/v1/transactions/submitwrite{ hash }
POST/v1/webhookswebhookssubscription + secret (shown once)
GET/v1/webhookswebhooksthis key’s subscriptions
DELETE/v1/webhooks/:idwebhooks{ deleted: true }

Look up a card

bash
curl $NOIR_API/v1/devices/$DEVICE_HASH -H "Authorization: Bearer $NOIR_KEY"
200 OK
{
  "deviceHash": "ab12…",
  "owner": "GBX7…Q4MZ",
  "policy": { "agent": "GDK2…", "maxAmount": "50000000", "asset": "CDLZ…", "expiresAt": null },
  "escrowBalance": "250000000"
}

Transactions: prepare → sign → submit

source is the acting account: the agent for authorize, the merchant for claim, the card owner for registerAgent, revokeAgent, fundEscrow and defundEscrow.

bash
curl -X POST $NOIR_API/v1/transactions/prepare -H "Authorization: Bearer $NOIR_KEY" \
  -H "Content-Type: application/json" -d '{
  "source": "GDK2…agent",
  "operation": {
    "kind": "authorize", "deviceHash": "ab12…", "merchant": "GMER…",
    "asset": "CDLZ…", "amount": "10000000"
  }
}'
# → { "xdr": "AAAAAgAAAAC…", "networkPassphrase": "Test SDF Network ; September 2015" }

Sign the XDR with the source account (Freighter, Wallets Kit, any Stellar SDK), then:

bash
curl -X POST $NOIR_API/v1/transactions/submit -H "Authorization: Bearer $NOIR_KEY" \
  -H "Content-Type: application/json" -d '{ "xdr": "<signed envelope>" }'
# → { "hash": "9f1c…" }   (returned once the network confirms)
Python — sign the prepared XDR
from stellar_sdk import Keypair, TransactionEnvelope
import requests

H = {"Authorization": f"Bearer {NOIR_KEY}"}
prep = requests.post(f"{API}/v1/transactions/prepare", headers=H, json={"source": agent.public_key, "operation": op}).json()
env = TransactionEnvelope.from_xdr(prep["xdr"], prep["networkPassphrase"])
env.sign(agent)
print(requests.post(f"{API}/v1/transactions/submit", headers=H, json={"xdr": env.to_xdr()}).json())

Errors

json
{ "error": { "code": "contract_refused", "contractError": "DuplicateAuth", "message": "paymentEscrow.authorize failed: DuplicateAuth (#6)" } }
StatuscodeWhen
400invalid_requestBad hash, address, amount or body
401unauthorizedMissing, malformed, unknown or disabled key
403insufficient_scopeKey lacks the endpoint’s scope
404not_found · device_not_found · agent_not_found
422contract_refusedIncludes contractError, e.g. DuplicateAuth
429rate_limitedSee Retry-After
502upstream_errorSoroban RPC failed

Production notes

  • Run behind TLS; set NOIR_API_CORS_ORIGINS only if browsers call it directly (prefer calling from your server so the key stays secret).
  • Keys, rate-limit buckets and subscriptions are single-process. For several replicas, move them to a shared store.
  • Webhook targets must be public https:// URLs — loopback and private networks are refused. See Webhooks.