Developer Hub · Quickstart
Customer API quickstart
Go from a new API client to a live response in about five minutes: read settled balances, then use the marketplace flow when that capability is enabled.
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. Marketplace listings, offers, and trades are available when the operator enables the marketplace capability. Writes cover marketplace offers and foreign-exchange quotes — requests that record intent and move no funds.
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
alongside per-transaction limits and an approved destination. Build the balance read 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.
Marketplace capability
Listings, offers, and trades are available only when the operator has enabled the
marketplace capability. These endpoints return 404 unless it is enabled. Marketplace
also requires the payments and invoicing capabilities, so ask the operator to enable
those prerequisites before you call the marketplace flow below.
Find a listing
Offers are made against a listing, so read the listings first and take an id from the
response. The call returns every listing your organisation owns plus every published
listing on the platform, and only a published listing accepts offers.
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer $LOAM_TOKEN" \
"$LOAM_API_BASE/listings?limit=2" | jq
# Response
{
"ok": true,
"data": [
{
"id": "94415e8e-1eb5-468a-b1d4-aa9a0473ec63",
"state": "published",
"unit_price": { "minor_units": 2600000, "currency": "USD" },
"created_at": "2026-07-02T11:03:44.902Z"
}
],
"page": { "next_cursor": null }
}Note the currency on the listing. An offer has to use the same one.
Create an offer
Take the listing id and currency from the previous response, generate a UUID for the
Idempotency-Key header, and post the price you are willing to pay — again as an
integer in the smallest unit, so 2500000 with USD offers USD 25,000.00.
A created offer returns 202 with its id and state, plus a Location header pointing
at the offer. 202 means accepted for processing: read the offer at that location to
see where it settled.
LISTING="$(
curl --fail-with-body --silent --show-error \
--header "Authorization: Bearer $LOAM_TOKEN" \
"$LOAM_API_BASE/listings?limit=100" \
| jq --compact-output '[.data[] | select(.state == "published")][0]'
)"
LISTING_ID="$(printf '%s' "$LISTING" | jq --raw-output ".id")"
LISTING_CURRENCY="$(printf '%s' "$LISTING" | jq --raw-output ".unit_price.currency")"
curl --fail-with-body --silent --show-error \
--request POST "$LOAM_API_BASE/offers" \
--header "Authorization: Bearer $LOAM_TOKEN" \
--header "Content-Type: application/json" \
--header "Idempotency-Key: $(uuidgen | tr '[:upper:]' '[:lower:]')" \
--data "$(
jq --null-input \
--arg listing_id "$LISTING_ID" \
--arg currency "$LISTING_CURRENCY" \
'{ listing_id: $listing_id, offered_price: { minor_units: 2500000, currency: $currency } }'
)" | jq
# Response
{
"ok": true,
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"state": "pending"
}
}A 409 here has two likely causes: the listing belongs to your own organisation, or the
offered currency does not match the listing. Pick a different published listing, or
correct the currency, rather than retrying the same body.
Retry 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 creating a second offer — 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
offers above, and they stay closed during preview. When they open, a request needs the
money.write scope granted by a human organisation owner under fresh multi-factor
authentication, a per-transaction limit, and a destination approved in advance.
Two failure modes are worth designing for now. A 403 means the destination is visible
but authorization was refused. A 400 means the destination is unknown or invisible to
the client. Neither is transient, so neither should be retried blindly.
Where to go next
- API reference — every endpoint, scope, schema, and error code.
- Settings → API clients — create, rotate, and revoke credentials.