Providers and flows
Every provider takes the same request and returns the same response, statuses, errors and webhooks. Send the provider as gateway, or leave it out and routing picks a connected one. Amounts are major units. Examples use fake values.
Three flows
- Redirect: send the customer to
authorizationUrl. - Phone approval:
authorizationUrlisnull; the customer approves on their phone and the result arrives by webhook. - Confirm on your page: Stripe wallets return
clientSecretandpublishableKey. See Payment initiate.
Redirect
Authorization: Bearer <app-token>
Content-Type: application/json
Idempotency-Key: <saved-unique-key>
{
"amount": 2500,
"currency": "NGN",
"gateway": "paystack",
"merchantReference": "ORDER-1",
"returnUrl": "https://shop.example/return",
"customerEmail": "[email protected]"
}{
"success": true,
"data": {
"paymentReference": "PAY-abc123",
"status": "pending",
"authorizationUrl": "https://checkout.paystack.com/abc123"
}
}Phone approval
Authorization: Bearer <app-token>
Content-Type: application/json
Idempotency-Key: <saved-unique-key>
{
"amount": 100,
"currency": "KES",
"gateway": "mpesa",
"merchantReference": "ORDER-2",
"returnUrl": "https://shop.example/return",
"customerPhone": "+254712345678"
}{
"success": true,
"data": {
"paymentReference": "ws_CO_07112026101530123456",
"status": "pending",
"authorizationUrl": null
}
}Wait for payment.completed or payment.failed. Query the payment if the webhook is late; a pending payment stays pending until the provider reports.
Each provider
| gateway | Meaning |
|---|---|
mpesa | Phone approval. Needs customerPhone. KES. |
mtnmomo | Phone approval. Needs customerPhone. Currencies follow your MTN account. |
paystack | Redirect. Needs customerEmail. NGN, GHS, ZAR, KES, USD. |
stripe | Redirect to Checkout, or Klarna with bnpl. Apple Pay and Google Pay confirm on your page. Needs customerEmail, except for wallets. USD, EUR, KES. |
pesapal | Redirect. Needs customerEmail or customerPhone. |
paypal | Redirect. Needs customerEmail or customerPhone. 20 currencies, including USD, EUR, GBP. |
adyen | Redirect to a payment link, or Klarna with bnpl. Needs customerEmail or customerPhone; bnpl needs customerEmail and country. |
dpo | Redirect. Needs customerEmail or customerPhone. USD, ZAR, KES, UGX, TZS, GHS. |
flutterwave | Redirect. Needs customerEmail. NGN, USD, EUR, GBP, GHS, KES, ZAR and 8 more. |
interswitch | Redirect. Needs customerEmail. NGN, Nigeria only. |
plaid_transfer | Redirect to Plaid. US bank debit in USD. Needs customerName, and customerEmail or customerPhone. |
plaid_payments | Redirect to Plaid. UK and EU bank payment in GBP, EUR, PLN, SEK, DKK, NOK. Needs customerName, and customerEmail or customerPhone. |
A missing customer field returns VALIDATION_ERROR (400) with the same wording on every provider. Your provider account decides which methods and currencies work. A provider is usable once Tinker Payments has enabled it and it is connected to your app; the capabilities call below shows which.
Country
country is the customer's ISO 3166-1 alpha-2 code, optional on every provider. Routing rules by country use it, and it comes back on the payment and its webhooks. Subscriptions and wallet top-ups accept it too. Adyen needs it for Klarna.
Klarna
Send paymentMethod bnpl on stripe or adyen. Both capture as soon as Klarna authorises, so ship within 24 hours. Adyen serves AT, DK, FI, DE, NO, SE, CH, NL, GB and US in that country's currency (EUR for AT, FI, DE and NL). A missing or unlisted country, or the wrong currency, returns VALIDATION_ERROR before any provider call. If Adyen reports a failed capture of the full amount, the payment becomes reversed and payment.reversed is sent. A failed partial capture is not reversed.
Authorization: Bearer <app-token>
Content-Type: application/json
Idempotency-Key: <saved-unique-key>
{
"amount": 49.9,
"currency": "EUR",
"gateway": "adyen",
"merchantReference": "ORDER-3",
"returnUrl": "https://shop.example/return",
"customerEmail": "[email protected]",
"country": "DE",
"paymentMethod": "bnpl"
}Plaid
customerName (the payer's legal name) is required on plaid_transfer and plaid_payments; a missing one returns VALIDATION_ERROR. On plaid_payments, the Plaid status INITIATED counts as paid. It is not proof the money arrived, and Plaid reports no later failure, so reconcile the bank credit by your reference if you need proof.
Authorization: Bearer <app-token>
Content-Type: application/json
Idempotency-Key: <saved-unique-key>
{
"amount": 40,
"currency": "GBP",
"gateway": "plaid_payments",
"merchantReference": "ORDER-4",
"returnUrl": "https://shop.example/return",
"customerEmail": "[email protected]",
"customerName": "Jane Doe",
"country": "GB"
}Ask before you charge
List the providers connected to your app, with a mode per feature: native, emulated, manual or unavailable. Both filters are optional.
{
"success": true,
"data": {
"providers": [
{
"gateway": "mpesa",
"name": "M-Pesa",
"methods": ["mobile_money"],
"currencies": ["KES"],
"countries": [],
"capabilities": {
"pay_mobile_money": "native",
"pay_apple_pay": "unavailable",
"refund": "native",
"partial_refund": "emulated"
}
}
]
}
}FEATURE_NOT_SUPPORTED
A request for an unavailable feature returns HTTP 422 before anything reaches the provider, so no money moves and no payment is stored. Retry on a gateway in details.alternatives; Tinker Payments never switches for you. details.gateway is absent when you named none.
{
"success": false,
"error": {
"code": "FEATURE_NOT_SUPPORTED",
"message": "M-Pesa doesn't support Apple Pay.",
"details": { "gateway": "mpesa", "feature": "apple_pay", "alternatives": ["stripe"] }
}
}Refunds and webhooks
Both work the same on every provider. A refund is a request Tinker Payments reviews; see Refunds. Results arrive as payment.* events; see Webhooks.
Credentials by plan
Pay-as-you-go apps always use Tinker's provider credentials; you connect nothing. Using your own provider credentials needs a paid plan. Saving your own on a pay-as-you-go app returns HTTP 403 PAID_PLAN_REQUIRED, and details.plan names the current plan (payg). A provider Tinker holds no credentials for shows default_available: false in the dashboard provider list; asking for Tinker's credentials for it returns HTTP 422 PLATFORM_CREDENTIALS_UNAVAILABLE with details.gateway. If an app moves back to pay-as-you-go, its own credentials are replaced by Tinker's where Tinker has them for that provider; otherwise the provider is disconnected.
{
"success": false,
"error": {
"code": "PAID_PLAN_REQUIRED",
"message": "Your own adyen credentials need a paid plan. Pay as you go uses Tinker's credentials.",
"details": { "plan": "payg" }
}
}