Sender authentication
Your users never create a Moov Money login. They’re already signed into your app. Your backend mints a short-lived token for that person and hands it to the SDK.
Authenticate those calls with an API key from the Moov Dashboard (Developers → API keys), using HTTP Basic auth. That is not the same credential as the signing secret on your ledger endpoints.
| Credential | Where you get it | Used for |
|---|---|---|
| API key | Dashboard → Developers → API keys | Participants and sender tokens |
| Signing secret | Funding setup | Encrypting callbacks to your ledger |
The key needs the /accounts/{your account}/transfers.write scope.
Without it, minting a token fails even if the username and password look
right.
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, with your own id as externalID. Do this
when they sign up, or when you first offer them payments and requests:
curl -X POST "https://api.moov.money/providers/$PROVIDER_ID/participants" \
-u "$MOOV_API_KEY_ID:$MOOV_API_KEY_SECRET" \
-H "Content-Type: application/json" \
-H "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"
},
"phoneNumbers": ["+15555550101"]
}'
address is required, and every address field except addressLine2
must be present. 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. A token request against a
still-pending participant is rejected with 403.
A repeat call with an externalID you’ve already registered returns 409.
Treat that as “already registered” and move on. 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 "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 scoped to this
one 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:
clientType | Use for |
|---|---|
web (default) | Browser-based flow built on the API |
device | The iOS or Android SDK |
service | Server-initiated payments or requests with no SDK presented |
Errors #
| Status | Meaning |
|---|---|
400 | App-key request missing externalID |
401 | Missing or invalid Basic auth credentials |
403 | Key lacks transfers.write, or the participant isn’t active (still pending, denied, or disabled) |
404 | externalID doesn’t match a registered participant; register it first |
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 #
- iOS SDK: hand the access token to
MoovMoneySDK. - Android SDK: same exchange, Kotlin side.
- Funding setup: the signing secret for ledger callbacks.