Entwickler-Hub · Schnellstart

Quickstart für die Customer API

Vom neuen API-Client zur Live-Antwort in etwa fünf Minuten: abgewickelte Salden lesen und anschließend den Marktplatz‑Flow nutzen, wenn diese Funktion aktiviert ist.

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.

Environment variables
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_SECRET

Get 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
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:

Capture the token in a shell
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:

GET /api/v1/balances
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.

GET /api/v1/listings
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.

POST /api/v1/offers
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