开发者中心 · 快速入门
Customer API 快速入门
在约五分钟内从新的 API 客户端获得实时响应:读取已结算余额,然后请求外汇报价。启用该功能时包含市场流程。
Every step below is one HTTPS request carrying a bearer token. There is no SDK to install and no browser redirect to handle.
The examples use curl, jq, and uuidgen. Any language works the same way:
form-encode the token request, then send Authorization: Bearer <token> on everything
else.
What preview access covers
Balances are available to every operator. A foreign-exchange quote is the preview write: it records provider-derived terms and moves no funds. Marketplace listings, offers, and trades are available only when the operator enables the marketplace capability.
Money movement stays closed during preview. Payments, bank transfers, and vault deposits
or withdrawals need the money.write scope, which an organisation owner grants by hand
under step-up authentication, alongside per-transaction limits and an approved
destination. Build the balance read and a quote now; the last section explains what
money endpoints will ask for.
Create an API client
Open Settings → API clients and create a client with the read
and write scopes. Loam displays the client secret once, at creation — copy it into a
secret manager before leaving the page, because rotating the client is the only way to
see a secret again.
The API answers on the same host you sign in to, under /api/v1. Requests to any other
host return 401 with invalid_client, even when the credentials themselves are valid.
export LOAM_API_BASE="https://<your-loam-host>/api/v1"export LOAM_CLIENT_ID="client_01J8YQ7N2M4P6R8T0V2X4Z6A8C"export LOAM_CLIENT_SECRET_FILE="$HOME/.config/loam/client-secret"umask 077mkdir -p "$(dirname "$LOAM_CLIENT_SECRET_FILE")"printf 'Paste the client secret: ' >&2read -r -s LOAM_CLIENT_SECRETprintf '\n' >&2printf '%s' "$LOAM_CLIENT_SECRET" > "$LOAM_CLIENT_SECRET_FILE"unset LOAM_CLIENT_SECRETGet an access token
Exchange the client credentials for a short-lived bearer token. The request body is form-encoded rather than JSON, and HTTP Basic authentication is deliberately not accepted.
A valid exchange returns 200 with the token, its remaining life in seconds, and the
scopes currently on the client:
POST /api/v1/oauth/token HTTP/1.1Host: your-loam-hostContent-Type: application/x-www-form-urlencodedgrant_type=client_credentials&client_id=client_01J8YQ7N2M4P6R8T0V2X4Z6A8C&client_secret=%3Cclient-secret-from-secret-manager%3E# Response{ "access_token": "access_token_value", "token_type": "Bearer", "expires_in": 3600, "scope": "read write"}An unknown, wrong, or revoked secret returns 401 with {"error": "invalid_client"},
and a client whose organisation has no API access returns 403 with
{"error": "unauthorized_client"}. Token failures use the OAuth 2 shape — error and
error_description — not the envelope the resource routes use.
Scopes are read from the client on every request, so narrowing them takes effect at once rather than when the token expires. Mint a new token when the old one runs out instead of storing it. In a shell, capture one now and reuse it for the rest of this guide:
LOAM_TOKEN="$(
curl --fail-with-body --silent --show-error \
--request POST "$LOAM_API_BASE/oauth/token" \
--header "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=$LOAM_CLIENT_ID" \
--data-urlencode "client_secret@$LOAM_CLIENT_SECRET_FILE" \
| jq --raw-output ".access_token"
)"Read your settled balances
Send the token as a bearer credential. This endpoint returns 200 with the standard
ok and data envelope:
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer $LOAM_TOKEN" \
"$LOAM_API_BASE/balances" | jq
# Response
{
"ok": true,
"data": [
{
"account_id": "6f9619ff-8b86-d011-b42d-00c04fc964ff",
"balance": 4250000,
"currency": "USD",
"last_settled_at": "2026-07-14T09:12:04.118Z"
}
]
}Balances are integers in the currency's smallest unit: 4250000 with USD means
USD 42,500.00. Never parse them as decimals.
An empty data array is a success, not an error — it means the organisation has no
settled balances yet.
Request a foreign-exchange quote
A quote is a write, not a money movement. The body is strict: from_currency,
to_currency, and amount are all required, the two currencies must differ, and
amount is a positive integer in the source currency's smallest unit — 10000 with
USD is USD 100.00. Every write needs an Idempotency-Key header.
The call returns 202 Accepted with a Location pointing at the quote. Creating a
quote does not move funds; execution is a separate capability and is not exposed by
this operation. When money.write opens, you reuse the quoted currencies and integer
amounts on a payment, ramp, or vault request.
curl --fail-with-body --silent --show-error \
--request POST "$LOAM_API_BASE/fx/quotes" \
--header "Authorization: Bearer $LOAM_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
--data '{"from_currency":"USD","to_currency":"EUR","amount":10000}'
// Response
HTTP/1.1 202 Accepted
Location: /api/v1/fx/quotes/a6bbdbeb-ed3b-42cb-85b1-af3112f2fe06
{
"ok": true,
"data": {
"id": "a6bbdbeb-ed3b-42cb-85b1-af3112f2fe06",
"from_amount": 10000,
"from_currency": "USD",
"to_amount": 9125,
"to_currency": "EUR",
"rate_decimal": "0.9125",
"fee_amount": -25,
"fee_currency": "USD",
"expires_at": "2099-08-14T16:35:00.000Z",
"state": "active"
}
}rate_decimal is a decimal string, not a float. fee_amount is a signed integer in
fee_currency minor units. state is active, consumed, or expired.
A malformed body, a missing field, or the same currency on both sides returns 400
with {"ok": false, "error": "invalid_input"}. A withdrawn API enablement or KYB
denial returns 403 with forbidden. Provider degradation returns 503 with
transient_error.
Read the quote back at the Location. The path is the quote id:
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer $LOAM_TOKEN" \
"$LOAM_API_BASE/fx/quotes/a6bbdbeb-ed3b-42cb-85b1-af3112f2fe06" | jqRetry writes with the same key
Send a retry with the same Idempotency-Key and the same body, and Loam replays the
original outcome instead of minting a second quote — so a timeout or a dropped
connection costs nothing. Reuse the key with a different body and the request is
rejected with 409. A new key always means a new operation.
Reads need no key. They change nothing and are safe to retry as they are.
Handle rate limits and errors
Token minting and resource requests are limited separately, per client. A 429 carries
Retry-After in seconds: wait that long, then retry the same request.
| Status | What to do |
|---|---|
400 |
Correct the request. Retrying the same body will fail the same way. |
401 |
Mint a new token. Rotate the secret only if it leaked. |
403 |
Ask an organisation owner to widen the grant. Do not retry. |
409 |
Read the resource, resolve the conflict, then send a new operation. |
429 |
Wait for Retry-After, then retry. |
503 |
Retry reads with backoff; retry writes with the same idempotency key. |
Resource errors carry {"ok": false, "error": "<code>"} with a machine-readable code.
Handle broad cases on the status and specific recovery paths on the code. The
API reference lists every code, the scope each endpoint needs,
and the full response schema.
Money movement
Payment, funding, and vault endpoints are held to a stricter standard than the reads and
quotes above, and they stay closed during preview. A quote (POST /api/v1/fx/quotes)
is the preview stand-in: it locks provider-derived terms under the write scope and
moves no funds. When money movement opens, you reuse those currencies and integer
amounts on a money.write request — there is no execute-quote route on this API.
money.write is not on the client by default. A human organisation owner grants it
under fresh step-up authentication (AAL2 / MFA at grant time), sets a per-transaction
limit, and approves each destination in advance. Narrowing or revoking the grant takes
effect on the next request. Machine execution does not prompt for a second factor; the
SQL money-authorization fence is the authority.
Two failure modes are worth designing for now. A 403 with forbidden means the
destination is visible but authorization was refused — missing scope, withdrawn
enablement, or a failed step-up grant. A 400 with invalid_input means the
destination is unknown or invisible to the client. A revoked destination returns 403
with destination_revoked. None of these are transient, so none should be retried
blindly.
When the grant is in place, a payment to an approved destination is:
curl --fail-with-body --silent --show-error \
--request POST "$LOAM_API_BASE/payments" \
--header "Authorization: Bearer $LOAM_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
--data '{"destination_id":"2f4cbb59-3ab1-4b6d-8d10-9cd2236cf94e","amount":2500000,"currency":"USD"}'
// Response
HTTP/1.1 202 Accepted
Location: /api/v1/payments/<payment-id>
{
"ok": true,
"data": {
"id": "<payment-id>",
"state": "processing"
}
}amount is a positive integer in minor units, the same convention as the quote.
destination_id must be an approved destination for this client. Poll
GET /api/v1/payments/<payment-id> for later state; GET /api/v1/payments lists the
organisation's payments.
Funds in from an approved bank account use source_id rather than a payee:
curl --fail-with-body --silent --show-error \
--request POST "$LOAM_API_BASE/onramp" \
--header "Authorization: Bearer $LOAM_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
--data '{"source_id":"7c501bfe-ffef-4670-9b8d-ed731e8ee3b2","amount":2500000,"currency":"USD"}'
// Response
HTTP/1.1 202 Accepted
Location: /api/v1/onramp/<onramp-id>
{
"ok": true,
"data": {
"id": "<onramp-id>",
"state": "<accepted-state>"
}
}Vault deposits and withdrawals take the vault id in the path and an amount in minor
units. The vault id is supplied with the API client; these routes do not list vaults.
curl --fail-with-body --silent --show-error \
--request POST "$LOAM_API_BASE/vaults/$VAULT_ID/deposits" \
--header "Authorization: Bearer $LOAM_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
--data '{"amount":2500000}'curl --fail-with-body --silent --show-error \
--request POST "$LOAM_API_BASE/vaults/$VAULT_ID/withdrawals" \
--header "Authorization: Bearer $LOAM_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
--data '{"amount":2500000}'Both return 202 with { ok: true, data: { id, state } } and a Location under the
same vault path. Poll that location for later state.
POST /api/v1/offramp is on the contract — destination_id, amount, and currency,
same envelope — and stays closed until destination screening and travel-rule
certification land. Do not call it during preview.
curl --fail-with-body --silent --show-error \
--request POST "$LOAM_API_BASE/offramp" \
--header "Authorization: Bearer $LOAM_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
--data '{"destination_id":"2f4cbb59-3ab1-4b6d-8d10-9cd2236cf94e","amount":2500000,"currency":"USD"}'Where to go next
- API reference — every endpoint, scope, schema, and error code.
- Settings → API clients — create, rotate, and revoke credentials.