curl --request POST \
--url https://sandbox-api.sakawave.example/v1/cards \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"customerId": "01K2Z3AB4C5D6E7F8G9HJKMNPQ",
"type": "virtual",
"brand": "mastercard",
"currency": "USD",
"externalReference": "card_issue_20260814_001",
"label": "Ada travel card",
"debitAccountId": "01K2Z3AB4D5E6F7G8H9JKMNPQR",
"amount": "5000",
"spendingControls": {
"transactionAmountLimit": "2000",
"dailyAmountLimit": "5000",
"dailyTransactionCount": 10
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
customerId: '01K2Z3AB4C5D6E7F8G9HJKMNPQ',
type: 'virtual',
brand: 'mastercard',
currency: 'USD',
externalReference: 'card_issue_20260814_001',
label: 'Ada travel card',
debitAccountId: '01K2Z3AB4D5E6F7G8H9JKMNPQR',
amount: '5000',
spendingControls: {
transactionAmountLimit: '2000',
dailyAmountLimit: '5000',
dailyTransactionCount: 10
}
})
};
fetch('https://sandbox-api.sakawave.example/v1/cards', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://sandbox-api.sakawave.example/v1/cards"
payload = {
"customerId": "01K2Z3AB4C5D6E7F8G9HJKMNPQ",
"type": "virtual",
"brand": "mastercard",
"currency": "USD",
"externalReference": "card_issue_20260814_001",
"label": "Ada travel card",
"debitAccountId": "01K2Z3AB4D5E6F7G8H9JKMNPQR",
"amount": "5000",
"spendingControls": {
"transactionAmountLimit": "2000",
"dailyAmountLimit": "5000",
"dailyTransactionCount": 10
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"card": {
"id": "01K2Z3AB4S5T6V7W8XYZABCDEF",
"label": "Ada travel card",
"type": "virtual",
"brand": "mastercard",
"currency": "USD",
"status": "active",
"maskedPan": "•••• •••• •••• 4242",
"expiryMonth": 8,
"expiryYear": 2028,
"holder": {
"type": "customer",
"id": "01K2Z3AB4C5D6E7F8G9HJKMNPQ",
"name": "Ada Okafor"
},
"balance": "5000",
"balanceFormatted": "50.00",
"spendingControls": {
"transactionAmountLimit": "2000",
"dailyAmountLimit": "5000",
"dailyTransactionCount": 10
},
"operations": [
{
"id": "01K2Z3AB4R5S6T7V8WXYZABCDE",
"cardId": "01K2Z3AB4S5T6V7W8XYZABCDEF",
"type": "issuance",
"status": "completed",
"sourceCurrency": "NGN",
"destinationCurrency": "USD",
"sourceAmount": "8000000",
"destinationAmount": "5000",
"failureCode": null,
"failureReason": null,
"completedAt": "2026-08-14T09:12:00.000Z",
"createdAt": "2026-08-14T09:11:40.000Z",
"updatedAt": "2026-08-14T09:12:00.000Z"
}
],
"createdAt": "2026-08-14T09:11:40.000Z",
"updatedAt": "2026-08-14T09:12:00.000Z"
},
"operation": {
"id": "01K2Z3AB4R5S6T7V8WXYZABCDE",
"cardId": "01K2Z3AB4S5T6V7W8XYZABCDEF",
"type": "issuance",
"status": "completed",
"sourceCurrency": "NGN",
"destinationCurrency": "USD",
"sourceAmount": "8000000",
"destinationAmount": "5000",
"failureCode": null,
"failureReason": null,
"completedAt": "2026-08-14T09:12:00.000Z",
"createdAt": "2026-08-14T09:11:40.000Z",
"updatedAt": "2026-08-14T09:12:00.000Z"
}
}{
"statusCode": 400,
"message": "Must be a valid identifier.",
"code": "validation_failed",
"errors": [
{
"message": "Must be a valid identifier.",
"field": "accountId",
"code": "custom",
"path": [
"accountId"
]
}
]
}{
"statusCode": 401,
"code": "INVALID_API_KEY",
"message": "Invalid or inactive API credentials."
}{
"statusCode": 403,
"message": "International wallets for your customers are not available yet. Your own approval cannot be used for a customer wallet.",
"code": "international_customer_wallet_not_supported",
"currency": "USD"
}{
"statusCode": 404,
"code": "resource_not_found",
"message": "Resource not found."
}{
"statusCode": 409,
"code": "resource_conflict",
"message": "The request conflicts with the current resource state."
}{
"statusCode": 429,
"message": "Too many API requests. Try again later.",
"code": "api_key_rate_limit_exceeded",
"retryAfterSeconds": 17
}{
"statusCode": 500,
"code": "internal_server_error",
"message": "An unexpected error occurred."
}{
"statusCode": 503,
"code": "provider_unavailable",
"message": "We could not issue an account number just now. Try again shortly."
}Issue a card
Issues a USD card and funds it in one call. Returns as soon as issuance is under way — the card starts in creating and becomes active once the provider has issued it.
curl --request POST \
--url https://sandbox-api.sakawave.example/v1/cards \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"customerId": "01K2Z3AB4C5D6E7F8G9HJKMNPQ",
"type": "virtual",
"brand": "mastercard",
"currency": "USD",
"externalReference": "card_issue_20260814_001",
"label": "Ada travel card",
"debitAccountId": "01K2Z3AB4D5E6F7G8H9JKMNPQR",
"amount": "5000",
"spendingControls": {
"transactionAmountLimit": "2000",
"dailyAmountLimit": "5000",
"dailyTransactionCount": 10
}
}
'const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
customerId: '01K2Z3AB4C5D6E7F8G9HJKMNPQ',
type: 'virtual',
brand: 'mastercard',
currency: 'USD',
externalReference: 'card_issue_20260814_001',
label: 'Ada travel card',
debitAccountId: '01K2Z3AB4D5E6F7G8H9JKMNPQR',
amount: '5000',
spendingControls: {
transactionAmountLimit: '2000',
dailyAmountLimit: '5000',
dailyTransactionCount: 10
}
})
};
fetch('https://sandbox-api.sakawave.example/v1/cards', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));import requests
url = "https://sandbox-api.sakawave.example/v1/cards"
payload = {
"customerId": "01K2Z3AB4C5D6E7F8G9HJKMNPQ",
"type": "virtual",
"brand": "mastercard",
"currency": "USD",
"externalReference": "card_issue_20260814_001",
"label": "Ada travel card",
"debitAccountId": "01K2Z3AB4D5E6F7G8H9JKMNPQR",
"amount": "5000",
"spendingControls": {
"transactionAmountLimit": "2000",
"dailyAmountLimit": "5000",
"dailyTransactionCount": 10
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text){
"card": {
"id": "01K2Z3AB4S5T6V7W8XYZABCDEF",
"label": "Ada travel card",
"type": "virtual",
"brand": "mastercard",
"currency": "USD",
"status": "active",
"maskedPan": "•••• •••• •••• 4242",
"expiryMonth": 8,
"expiryYear": 2028,
"holder": {
"type": "customer",
"id": "01K2Z3AB4C5D6E7F8G9HJKMNPQ",
"name": "Ada Okafor"
},
"balance": "5000",
"balanceFormatted": "50.00",
"spendingControls": {
"transactionAmountLimit": "2000",
"dailyAmountLimit": "5000",
"dailyTransactionCount": 10
},
"operations": [
{
"id": "01K2Z3AB4R5S6T7V8WXYZABCDE",
"cardId": "01K2Z3AB4S5T6V7W8XYZABCDEF",
"type": "issuance",
"status": "completed",
"sourceCurrency": "NGN",
"destinationCurrency": "USD",
"sourceAmount": "8000000",
"destinationAmount": "5000",
"failureCode": null,
"failureReason": null,
"completedAt": "2026-08-14T09:12:00.000Z",
"createdAt": "2026-08-14T09:11:40.000Z",
"updatedAt": "2026-08-14T09:12:00.000Z"
}
],
"createdAt": "2026-08-14T09:11:40.000Z",
"updatedAt": "2026-08-14T09:12:00.000Z"
},
"operation": {
"id": "01K2Z3AB4R5S6T7V8WXYZABCDE",
"cardId": "01K2Z3AB4S5T6V7W8XYZABCDEF",
"type": "issuance",
"status": "completed",
"sourceCurrency": "NGN",
"destinationCurrency": "USD",
"sourceAmount": "8000000",
"destinationAmount": "5000",
"failureCode": null,
"failureReason": null,
"completedAt": "2026-08-14T09:12:00.000Z",
"createdAt": "2026-08-14T09:11:40.000Z",
"updatedAt": "2026-08-14T09:12:00.000Z"
}
}{
"statusCode": 400,
"message": "Must be a valid identifier.",
"code": "validation_failed",
"errors": [
{
"message": "Must be a valid identifier.",
"field": "accountId",
"code": "custom",
"path": [
"accountId"
]
}
]
}{
"statusCode": 401,
"code": "INVALID_API_KEY",
"message": "Invalid or inactive API credentials."
}{
"statusCode": 403,
"message": "International wallets for your customers are not available yet. Your own approval cannot be used for a customer wallet.",
"code": "international_customer_wallet_not_supported",
"currency": "USD"
}{
"statusCode": 404,
"code": "resource_not_found",
"message": "Resource not found."
}{
"statusCode": 409,
"code": "resource_conflict",
"message": "The request conflicts with the current resource state."
}{
"statusCode": 429,
"message": "Too many API requests. Try again later.",
"code": "api_key_rate_limit_exceeded",
"retryAfterSeconds": 17
}{
"statusCode": 500,
"code": "internal_server_error",
"message": "An unexpected error occurred."
}{
"statusCode": 503,
"code": "provider_unavailable",
"message": "We could not issue an account number just now. Try again shortly."
}customerId is required for a business client, and that customer must be at tier 2 or above — but every customer is created at tier 1 and no endpoint raises it. Every attempt therefore ends in 404 card_holder_not_found. This is recorded as an open contract decision.What issuance does
Four things happen in one call: a hidden USD balance account is opened for the card, the card is created at the provider, your funding account is debited, and the card is funded. Theoperation in the response tracks all of it.
Idempotency, and retrying a failure
externalReference is the key. Reusing it returns the existing card and operation — except when the previous issuance failed, in which case the same reference re-drives it rather than returning the failure.
Funding
debitAccountId and amount are required — there is no unfunded card. amount is in the card’s own currency, so USD minor units. The account must be one of your own; a customer wallet is refused with 403 card_client_account_required.
When that account is not in the card’s currency, Sakawave quotes the FX itself at issuance and debits the equivalent — you do not pass a quote. POST /v1/card-fx-quotes is still there if you want to show an indicative rate first, but issuance does not take one. Funding and withdrawal on an existing card do.
Types
type | Behaviour |
|---|---|
virtual | Reusable. Supports spending controls. |
disposable | Single use — moves to used after its first payment. Spending controls are rejected. |
Error codes
| Status | code | Cause |
|---|---|---|
400 | validation_failed | A field failed validation, or spending controls were sent for a disposable card. |
400 | card_holder_required | A business client did not send a customerId. |
403 | — (reason: feature_not_available) | Your business is below tier 2, which card issuance requires. |
403 | card_client_account_required | The funding account is a customer wallet. |
404 | card_holder_not_found | The customer does not exist, is not an individual, is below tier 2, or is missing a name, email, or phone number. |
409 | card_account_ineligible | The funding account is missing, inactive, or cannot be debited. |
409 | insufficient_funds | The funding account cannot cover the amount. |
503 | card_provider_unavailable | The card provider could not be reached. |
503 | invalid_card_fx_quote | The FX provider returned an incomplete quote for the conversion. |
401 | INVALID_API_KEY | The key is missing, invalid, inactive, expired, from the other environment, or blocked by its IP allowlist. |
404 | card_not_found | No such card for this business. |
429 | api_key_rate_limit_exceeded | The per-minute limit for this key was exceeded. |
Authorizations
A secret API key, sent as Authorization: Bearer sk_test_… in sandbox or Bearer sk_live_… in live.
Body
A card is always issued funded. A disposable card cannot carry spending controls.
virtual, disposable mastercard USD Your idempotency key for the issuance operation.
1 - 1201 - 80One of your own accounts. A customer wallet is refused.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
A positive integer number of minor units.
^[1-9]\d*$"2500000"
Required for a business client. The customer must be an individual at tier 2 or above with a full name, email, and phone number.
26^[0-7][0-9A-HJKMNP-TV-Z]{25}$"01K2Z3AB4C5D6E7F8G9HJKMNPQ"
Per-card limits, in USD minor units. Not available on a disposable card.
Show child attributes
Show child attributes