Sender authentication

Your users never create a Moov Money login — they’re already signed in to your app. Your backend mints a short-lived token for that user and hands it to the SDK.

Authenticate those calls with an API key from the Moov Dashboard (Developers → API keys), using HTTP Basic auth. An API key is not the same credential as the signing secret on your ledger endpoints.

CredentialWhere you get itUsed for
API keyDashboard → Developers → API keysParticipants and sender tokens
Signing secretFunding setupEncrypting callbacks to your ledger

The key needs the /accounts/{your account}/transfers.write scope. Without the scope, minting a token fails even if the username and password seem correct.

Authenticate with Basic auth #

Do not exchange the API key for a Bearer token first. Send the public/secret pair straight to api.moov.money:

Authorization: Basic base64(MOOV_API_KEY_ID:MOOV_API_KEY_SECRET)

curl -u "$MOOV_API_KEY_ID:$MOOV_API_KEY_SECRET" does the encoding. Only Basic auth works on /provider-participant-token. The usual Moov OAuth2 exchange at api.moov.io/oauth2/token does not.

Register the sender as a participant #

Register each user once, using your own ID as externalID. Complete this step when they sign up, or when you first offer payments and requests:

curl -X POST "https://api.moov.money/providers/$PROVIDER_ID/participants" \
  -u "$MOOV_API_KEY_ID:$MOOV_API_KEY_SECRET" \
  -H "X-Moov-Version: v2026.10.00" \
  -H "Content-Type: application/json" \
  -H "X-Idempotency-Key: $(uuidgen)" \
  --data '{
    "externalID": "user-abc-123",
    "givenName": "Jane",
    "familyName": "Smith",
    "address": {
      "addressLine1": "123 Main St",
      "city": "San Francisco",
      "stateOrProvince": "CA",
      "postalCode": "94105",
      "country": "US"
    },
    "phones": [{ "countryCode": "1", "number": "5555550101" }]
  }'

address is required, and every address field except addressLine2 must be present. At least one phone is required: countryCode without the +, number as digits only. X-Idempotency-Key is required; reuse it when you retry the same create. The 201 returns the assigned participantID.

Creation is asynchronous: the participant starts pending while a signup fraud check runs, then moves to active or denied. Only active participants can obtain a sender token, so poll GET /providers/{providerID}/participants/{participantID} (same Basic auth) rather than minting a token immediately after registering. The path takes your externalID or Moov’s participantID. A token request against a still-pending participant is rejected with 403.

A repeat call with an externalID you’ve already registered returns 201 with the existing participantID; the stored profile is not changed. A 409 means an idempotency conflict: the key was reused with a different body, or the first request is still running.

Field-by-field: create participant.

Mint a sender token for the signed-in user #

Same Basic auth. Identify the user with the externalID you registered:

curl -X POST "https://api.moov.money/provider-participant-token" \
  -u "$MOOV_API_KEY_ID:$MOOV_API_KEY_SECRET" \
  -H "X-Moov-Version: v2026.10.00" \
  -H "Content-Type: application/json" \
  --data '{
    "externalID": "user-abc-123",
    "clientType": "device"
  }'
{
  "access_token": "eyJhbGciOiJFZERTQSIs...",
  "token_type": "bearer",
  "expires_in": 3600
}

Hand access_token to the iOS or Android SDK. It is specifically scoped to this user and lasts about an hour (expires_in).

Field-by-field: create sender token.

Re-mint when it expires #

Mint another token the same way. There is no refresh flow to build around: the response carries no refresh_token.

Client types #

clientType defaults to web when omitted. Set it to match the surface the token is for:

clientTypeUse for
web (default)Browser-based flow built on the API
deviceThe iOS or Android SDK
serviceServer-initiated payments or requests with no SDK presented

Errors #

StatusMeaning
400App-key request missing externalID
401Missing or invalid Basic auth credentials
403Key lacks transfers.write, or the participant isn’t active (still pending, denied, or disabled)
404externalID doesn’t match a registered participant, or X-Moov-Version is missing (the default version predates this endpoint)

Creating a participant adds two more: 400 when X-Idempotency-Key is missing or malformed, and 422 when the body fails validation.

Responses that carry a body use RFC 9457 problem details, served as application/problem+json. Branch on type, a stable URI such as https://api.moov.money/errors#invalid-request. Treat it as an opaque constant; title and detail are prose for humans and may be reworded. Validation failures list each rejected field under errors, with a JSON Pointer in pointer:

{
  "type": "https://api.moov.money/errors#invalid-request",
  "title": "Bad Request",
  "status": 400,
  "errors": [
    { "pointer": "/externalID", "detail": "must not be empty" }
  ]
}

401, 403, and 404 have no body. Errors are intentionally generic: a bad credential and a participant that belongs to a different provider both come back as 403, so a caller probing with a stolen credential learns nothing from the status code.

Next steps #