Quick start

This guide walks through the first payment, end to end: a user on your books, a source to pull from, a payout, then a hold on your ledger.

Payments are funded from deposits your user already holds with you: you host authorize, capture, and release, and Moov calls them as the payment moves. See flow of funds.

You will need a Moov API key (Dashboard → Developers → API keys), your provider ID (your Moov account ID), a public HTTPS URL Moov can POST to, and a test deposit account on your core.

Register ledger URLs and a signing secret #

Give Moov the URLs you will host, then fetch the wrapping key. Moov POSTs to each URL as-is:

URLWhen
AuthorizationURLThe user sends
CaptureURLThe recipient claims
HoldReleaseURLThe payment is canceled or expires

Add CreditURL later if you collect on requests. How to register the URLs, the JWE shape, and a decrypt/encrypt example: funding setup and encryption.

Issue the secret once. Decode the base64url value to 32 raw bytes and store it. You cannot fetch it again.

curl -X POST "https://api.moov.money/providers/$PROVIDER_ID/signing-secrets" \
  -u "$MOOV_API_KEY_ID:$MOOV_API_KEY_SECRET"

Create signing secret. A first stub can always return approved with a holdReference you mint. Wire real balances after the round trip works.

Create a user #

Register each sender once, with your id as externalID. Do this when they sign up, or the first time you offer them send:

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, matching what you already collect for a recipient. The 201 returns the assigned participantID.

They start pending while a signup check runs, then active or denied. Only active can send. Poll rather than minting a token straight away:

curl "https://api.moov.money/providers/$PROVIDER_ID/participants/user-abc-123" \
  -u "$MOOV_API_KEY_ID:$MOOV_API_KEY_SECRET"

A repeat create with the same externalID returns 409. Treat that as already registered. Create participant, get participant.

Add a funding source #

Tell Moov which account they can send from. providerSourceID is your id for that account. You will see it again on authorize as fundingSource:

curl -X POST "https://api.moov.money/providers/$PROVIDER_ID/participants/user-abc-123/funding-sources" \
  -u "$MOOV_API_KEY_ID:$MOOV_API_KEY_SECRET" \
  -H "Content-Type: application/json" \
  --data '{
    "fundingType": "bank_account",
    "providerSourceID": "checking-1234",
    "displayName": "Premier Checking",
    "last4": "1234"
  }'

The response includes a fundingSourceID. Creation is idempotent on (participant, fundingType, providerSourceID), so re-sending an existing source returns the same id. Create funding source.

You can skip this call entirely: the create-participant body takes an optional fundingSources array (up to 10), so the previous step and this one can be one request. The 201 doesn’t echo them back; list the participant’s funding sources to confirm.

Send the first payout #

The payment happens in your app, through the SDK. There is no provider-authenticated payout endpoint: payouts are created against the sender’s own session. Mint a sender token (clientType: device) for the signed-in user and present the iOS or Android SDK. The SDK resolves the funding sources you registered above from the token, so there is nothing further to pass in. They pick a person and an amount; the SDK creates the payout. Moov then hits your authorize URL.

The token is the only part you build. Mint it with your API key:

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" }'

Create sender token. Hand access_token to the SDK; it creates the payout against that sender’s session. You never post the payout yourself.

Your signal that it worked is the authorize call landing on your ledger, in the next step. The recipient gets a claim link good for 10 days. A payment your ledger refuses, or that the fraud check denies, ends as a denied payout with a reason the SDK surfaces to your app; see trust and safety.

Watch the ledger #

The payout is what the SDK created. The ledger is what Moov calls you.

  1. Authorize fires as the payment is created. Decrypt the JWE. Under moov you get the payout ID, externalID, fundingSource (your providerSourceID), the amount, and a fraudScore. Place a hold on that account. Return approved and a holdReference you can look up later, echoing idempotencyKey. Authorize.
  2. Capture fires when the recipient finishes the claim. Moov sends back your holdReference. Debit the hold, return approved and a captureReference. Capture.
  3. Release fires if they cancel or the 10-day window lapses. The hold comes off; nothing is captured. Release.

Inner JWT: iat and exp at the top, Moov fields under moov. Encryption has the unwrap. Outcomes, retries, and what to persist: ledger endpoints and idempotency.

A hold ends in exactly one of capture or release. If authorize never arrived, the payout did not land on your core.

Next #