Getting Started

Credentials, base URL, required headers and your first call.

Credentials

You generate your own API key and secret in the Alfred dashboard.

https://dashboard.alfredpay.io

Nobody at Alfred issues them to you, sends them to you, or keeps a copy.

That is deliberate. Your secret exists only where you generated it and wherever you choose to store it, so there is no internal copy of it anywhere and no handover for anyone to intercept. It also means we cannot recover a secret you have lost. If that happens, generate a new pair and retire the old one.

Never send a secret to Alfred. That includes support tickets, shared documents and your account manager. Anyone asking you for it is not us.

Treat it the way you'd treat any other credential. Keep it on your backend, out of source control, and out of any client-side application. Make it rotatable without a deploy.

Keys are scoped to what your account is permitted to do. A key that can collect but not pay out will be refused on the payout endpoints, and that refusal is a permission error rather than a validation one.

Base URL

https://api.sandbox.alfredpay.io

Sandbox is contract-identical to production. Requests, responses and event payloads are the same on both. The only difference is the simulators under /v1/test/, which exist on sandbox and return 404 not_found on production.

The simulators run the real pipeline without real settlement, so you can take a flow end to end as many times as you need.

The production host becomes available once your account is enabled for production. Read both hosts from configuration rather than hardcoding them, so moving from sandbox to production is a config change rather than a deploy.

Authenticating

There are two options. Pick one and use it consistently.

The first is a bearer token.

Authorization: Bearer <api_key>

The second is a key and secret pair. Both headers have to be on the same request, because neither works on its own.

api-key: <api_key>
api-secret: <api_secret>

Headers on every write

Idempotency-Key is required on every POST that changes something, including the sandbox simulators. The value must be a UUID, and any other format is refused. A POST sent without one is refused with 400 idempotency_key_required.

Replaying the same key with the same body returns the original response, with the original status code and an Idempotent-Replayed: true header. The same key with a different body is a 409 idempotency_conflict. Keys are kept for 24 hours and scoped to your API key.

One endpoint is exempt. POST /v1/customers/{customerId}/verifications/preflight changes nothing, takes no key, and always returns a fresh evaluation. Sending a key there is ignored rather than rejected.

Your first call

Read what countries are supported. It needs no setup and tells you what you can actually do.

curl https://api.sandbox.alfredpay.io/v1/countries \
  -H "Authorization: Bearer $ALFRED_API_KEY"

The response lists each supported country with its currencies and its local stablecoin where one exists.

{
  "data": [
    {
      "code": "MEX",
      "name": "Mexico",
      "currencies": ["MXN", "USD"],
      "localStablecoin": "MXNa"
    },
    {
      "code": "ARG",
      "name": "Argentina",
      "currencies": ["ARS", "USD"],
      "localStablecoin": "ARSa"
    }
  ]
}

Then read one of them in full. You'll come back to this call more than any other, because it carries the route registry. The registry lists every supported combination of currency, destination type and rail. For each one it gives the fields to collect and whether a payout on it needs a quote.

curl https://api.sandbox.alfredpay.io/v1/countries/ARG \
  -H "Authorization: Bearer $ALFRED_API_KEY"

The response below is abridged to one route. A real response carries every route the country supports.

{
  "code": "ARG",
  "name": "Argentina",
  "currencies": ["ARS", "USD"],
  "localStablecoin": "ARSa",
  "routes": [
    {
      "country": "ARG",
      "currency": "ARS",
      "destinationType": "BANK_ACCOUNT",
      "rail": "COELSA",
      "status": "LIVE",
      "operations": ["PAYOUT", "COLLECTION"],
      "identifierTypes": ["CBU", "CVU", "ALIAS"],
      "fields": [
        { "name": "holderName", "required": true },
        { "name": "identifier", "required": true },
        { "name": "taxId", "required": true }
      ],
      "quote": {
        "sameCurrency": "NOT_REQUIRED",
        "crossCurrency": "REQUIRED"
      },
      "limits": { "perTransaction": "5000000.00", "daily": "20000000.00" }
    }
  ]
}

Build your registration form from fields rather than from a list you keep yourself. Check status before you rely on a route, because only LIVE accepts production traffic.

Country codes are ISO 3166-1 alpha-3, so ARG rather than AR. If you can only send alpha-2 we normalize it at the boundary, but everything we return, store and report uses alpha-3.

Conventions you'll hit immediately

  • Pagination: ?limit= defaults to 50 and caps at 100. Use ?cursor= with the next_cursor from the previous response, and check has_more.
  • Rate limits: Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. A 429 carries Retry-After. Read the headers rather than assuming a number.
  • Money: Amounts are string decimals with an explicit currency, or an asset plus a chain. Alfred never uses floats.
  • Timestamps: All timestamps are RFC 3339 and in UTC.
  • Identifiers: Identifiers are opaque strings, so don't parse them. customerId is always a path segment and never a body field or a query filter. Calls on your own account, such as GET /v1/balances and POST /v1/linked_accounts, take no customerId at all.

If you integrated against the previous API

Skip this section if this is your first Alfred integration.

Creating a customer works differently now. The old API classified a customer at creation and took a country field. This one takes type as the only required field, with name and email optional.

curl -X POST https://api.sandbox.alfredpay.io/v1/customers \
  -H "Authorization: Bearer $ALFRED_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "type": "INDIVIDUAL", "email": "[email protected]" }'
{
  "id": "cus_...",
  "type": "INDIVIDUAL",
  "status": "NOT_STARTED",
  "createdAt": "2026-09-23T14:02:11Z"
}

type is either INDIVIDUAL or BUSINESS. It decides whether the customer runs KYC or KYB and it can't be changed later. Everything else that classifies the customer, including country, is set afterwards with PUT /v1/customers/{customerId}. Sending country on creation will fail validation.

Three other differences matter when porting. Paths are now prefixed with /v1/. The partner is identified by your API key, so you never pass your own identifier. Onramp and offramp are no longer single endpoints, and a pay-in, a conversion and a payout are separate transactions related by a payment_flow_id you supply. See Overview for how that flow now looks.

Before you build anything else

Register a webhook endpoint. Almost every flow has a step that's only correct once an event has arrived, and building against polling will mean rewriting later. See Webhooks and events.

Sandbox accounts start with nothing and you cannot transfer funds into one. To put money on a customer you simulate a deposit. See Testing in sandbox.


Did this page help you?