Card Tokenization (Consent Link)
Card Tokenization lets you store a customer's card for future use — without ever handling, seeing, or encrypting their card details yourself.
Instead of collecting card data directly, you generate a consent link and send it to your customer. They open it, see clearly which business is asking to store their card, agree, and enter their card details on a page PayOnUs hosts and secures. Once they're done, you're notified and can retrieve the resulting card token.
There is no API to tokenize a card directly. Every card token is created through this consent-link flow — this is intentional: it guarantees a customer has explicitly agreed to let your business store their card before any tokenization happens.
How It Works
- You create a link — call Create Card Tokenization Link with your
businessId, the customer's details, and your ownmerchantReferencefor the transaction/order this token relates to. - You send the link to your customer — the API returns a
url. Send it however you'd reach your customer (SMS, email, chat, in-app message). How you deliver it is entirely up to you. - Your customer opens the link — they land on a PayOnUs-hosted page showing your business name and a plain-language consent notice explaining that they're authorizing your business to store their card for future charges.
- Your customer reviews and submits — they check the consent box, enter their card details, and submit. Card data is encrypted in the customer's browser and never touches your servers.
- PayOnUs tokenizes the card — once consent and card details are validated, the card is tokenized with the payment gateway. Only a masked PAN and a gateway token are stored — the raw card number is never persisted. Your customer sees a success or failure page.
- You're notified — PayOnUs sends a webhook to your configured webhook URL once the link completes (see Webhook Notification).
- You retrieve the token — look up the link's status by its
idor by your ownmerchantReference, search across your links with more filters, or search all of your business's card tokens directly. - You charge the card whenever you need to — call Charging a Tokenized Card with the token's
idand an amount. No further action from the customer is needed. - You revoke the card whenever it's no longer needed — call Delete a Card Token to permanently remove it, both with us and at the payment gateway.
Merchant Customer's browser PayOnUs
──────── ────────────────── ───────
POST /card-tokenization-links ──────────────────────────────────────► creates link (PENDING)
│
│ { url, merchantReference, expiresAt }
▼
send `url` to customer
(SMS / email / chat)
│
│ opens `url`
│ ─────────────────────────────────► hosted consent + card page
│ reviews consent, enters card
│ submits ─────────────────────────► validates, tokenizes, stores
│ masked PAN + gateway token
│ sees success/failure page ◄────────
│
│ ◄─────────────────────────────────────────────────────── webhook: link completed
│
GET /card-tokenization-links/{id} or GET /card-tokens (search)
Create Card Tokenization Link
Request
- Path:
/api/v1/card-tokenization-links - Method: POST
Request Body Table
| Field | Type | Required | Description |
|---|---|---|---|
| businessId | string | Yes | Your Business ID |
| customer | object | Yes | Customer information |
| customer.name | string | Yes | Customer's full name |
| customer.email | string | Yes | Customer's email address |
| customer.phone | string | No | Customer's phone number, international format |
| customer.externalId | string | No | Your own identifier for this customer, if you have one |
| merchantReference | string | Yes | Your own reference for this tokenization request (e.g. an order or subscription ID). Max 100 characters. |
| expiryMinutes | number | No | How long the link stays valid, in minutes. Defaults to 30, capped at 1440 (24 hours) if a larger value is supplied. |
Sample Request
{
"businessId": "{business_id}",
"customer": {
"name": "Jane Doe",
"email": "jane.doe@example.com",
"phone": "+2348012345678"
},
"merchantReference": "order-58213",
"expiryMinutes": 30
}
Sample Response
{
"status": 200,
"message": "Success",
"data": {
"id": "xxx-xxx-xxx",
"url": "https://core-sandbox.payonus.com/card-consent/aB3dEfGhIj...",
"expiresAt": "2026-09-12 15:30:00",
"status": "PENDING",
"merchantReference": "order-58213",
"businessName": "Acme Technologies Ltd",
"merchantName": "Acme Technologies Ltd",
"businessId": "xxx-xxx-xxx"
}
}
Send your customer the url value — that's the entire integration on your side for the consent-collection step. You do not need to build a card-capture form, and you never receive or handle card data.
Possible Errors
| Status | Meaning |
|---|---|
| 403 | Card tokenization is not enabled for your business. Contact support. |
| 409 | You already have a link with this exact merchantReference for this business - regardless of its status. Use a different reference for a new request. |
Get Link Status
You can check on a link two ways — by the id returned when you created it, or by your own merchantReference.
By ID
- Path:
/api/v1/card-tokenization-links/{id} - Method: GET
- Query Parameter:
businessId(required)
By Merchant Reference
- Path:
/api/v1/card-tokenization-links/by-reference/{merchantReference} - Method: GET
- Query Parameter:
businessId(required)
Since a merchantReference can only ever be used once per business (see below), this returns the single link created with that reference.
Sample Response
Same shape as the create response above, reflecting the link's current status.
Link Statuses
| Status | Meaning |
|---|---|
| PENDING | Link is active and awaiting the customer - this includes a customer who has already tried and failed once (e.g. an invalid card, or a momentary gateway issue) and can try again |
| COMPLETED | Customer consented and the card was tokenized successfully |
| EXPIRED | Customer did not complete the link before expiresAt |
| CANCELLED | Link was cancelled after too many submission attempts |
A failed submission attempt (invalid card details, a declined card, a temporary gateway issue, etc.) does not end the link - it's deliberately left PENDING so your customer can simply try again with the same link, rather than you having to generate and re-send a new one. A link only stops being usable when it expires, is cancelled after too many attempts, or completes successfully.
A link is single-use: it can't be reused after it's COMPLETED, EXPIRED, or CANCELLED. If you need a link for the same order after one of those, create a new one with a different merchantReference - a reference is permanently tied to the link it was first used for and can never be reused for that business, regardless of that link's status.
Search Card Tokenization Links
Look up your consent links with more flexible filtering than the two lookups above — useful for building a dashboard view, reconciling a batch of orders, or checking on links across a date range.
- Path:
/api/v1/card-tokenization-links - Method: GET
Query Parameters (all optional except where noted)
| Parameter | Type | Description |
|---|---|---|
| merchantId | string | Your Merchant ID. Required for merchant API-key access. |
| businessIds | array | One or more Business IDs to restrict the search to (defaults to all your businesses if omitted) |
| id | string | A specific link's ID |
| merchantReference | string | Your own reference from link creation |
| status | string | One of the link statuses above |
| hasCardToken | boolean | true to only return links that have generated a card token; false for links that haven't (yet) |
| createdFrom | date | Only links created on/after this date (yyyy-MM-dd) |
| createdTo | date | Only links created on/before this date (yyyy-MM-dd) |
| pageNo | number | Page number, starting at 1 (default: 1) |
| pageSize | number | Results per page (default: 30) |
Sample Response
{
"status": 200,
"message": "Success",
"data": {
"pageNo": 1,
"pageSize": 30,
"lastPage": 1,
"totalNumberOfItems": 1,
"content": [
{
"id": "xxx-xxx-xxx",
"merchantReference": "order-58213",
"status": "COMPLETED",
"customerName": "Jane Doe",
"customerEmail": "jane.doe@example.com",
"cardTokenId": "xxx-xxx-xxx",
"expiresAt": "2026-09-12 15:30:00",
"createdDate": "2026-09-12 15:05:22",
"businessName": "Acme Technologies Ltd",
"merchantName": "Acme Technologies Ltd",
"businessId": "xxx-xxx-xxx"
}
]
}
}
cardTokenId is null until the link completes — combine hasCardToken=true with a createdFrom/createdTo range if, for example, you want to reconcile which of last week's links actually produced a usable token.
Webhook Notification
When a link completes successfully, PayOnUs sends a notification to your configured webhook URL (see Webhook Notifications for how to set this up):
{
"event": "card_tokenization_link.completed",
"cardTokenizationLinkId": "xxx-xxx-xxx",
"cardTokenId": "xxx-xxx-xxx",
"businessId": "xxx-xxx-xxx",
"status": "COMPLETED"
}
This notification is currently only sent for successful completions, is delivered once, and is not retried if your endpoint is unreachable. We recommend also checking link status via the API above (or the search endpoint below) rather than relying on the webhook alone, especially for anything time-sensitive.
Search Card Tokens
Look up previously created card tokens for your business.
- Path:
/api/v1/card-tokens - Method: GET
Query Parameters (all optional except where noted)
| Parameter | Type | Description |
|---|---|---|
| merchantId | string | Your Merchant ID. Required for merchant API-key access. |
| businessIds | array | One or more Business IDs to restrict the search to (defaults to all your businesses if omitted) |
| id | string | A specific card token's ID |
| requestId | string | The internal request ID recorded against the token |
| merchantReference | string | Your merchantReference from the tokenization link that produced the token |
| customerEmail | string | Filter by the customer's email |
| createdFrom | date | Only tokens created on/after this date (yyyy-MM-dd) |
| createdTo | date | Only tokens created on/before this date (yyyy-MM-dd) |
| pageNo | number | Page number, starting at 1 (default: 1) |
| pageSize | number | Results per page (default: 30) |
Sample Response
{
"status": 200,
"message": "Success",
"data": {
"pageNo": 1,
"pageSize": 30,
"lastPage": 1,
"totalNumberOfItems": 1,
"content": [
{
"id": "xxx-xxx-xxx",
"maskedPan": "512345******1234",
"accountNumber": "1234567890",
"tokenExpiry": "2030-01-01",
"paymentGateway": "WEMABANK",
"requestId": "ONUS-CTOKEN-...",
"customerName": "Jane Doe",
"customerEmail": "jane.doe@example.com",
"merchantReference": "order-58213",
"createdDate": "2026-09-12 15:05:22",
"businessName": "Acme Technologies Ltd",
"merchantName": "Acme Technologies Ltd",
"businessId": "xxx-xxx-xxx"
}
]
}
}
merchantReference is populated when the token came from a consent link; it's null for any legacy tokens created before this flow existed.
Charging a Tokenized Card
Once you have a card token — from a completed consent link, or found via Search Card Tokens — you can charge it directly. Because the cardholder already went through consent and card capture once, there's no OTP, PIN, or 3DS step for the customer to complete this time: the charge is attempted and you get a final status straight away.
Request
- Path:
/api/v1/card-payments/with-token - Method: POST
Request Body Table
| Field | Type | Required | Description |
|---|---|---|---|
| businessId | string | Yes | Your Business ID. Must be the same business the card token belongs to. |
| cardTokenId | string | Yes | The id of a previously created card token (the id field from Search Card Tokens or the tokenization result). |
| amount | number | Yes | Amount to charge. Minimum 1. |
| reference | string | Yes | Your own unique reference for this charge. Must not already be in use for this business. |
| narration | string | No | Optional transaction narration |
| notificationUrl | string | No | Optional webhook override for this transaction. Notification is sent to the URL configured on the portal if not set. |
Sample Request
{
"businessId": "{business_id}",
"cardTokenId": "{card_token_id}",
"amount": 5000,
"reference": "charge-order-58213-1",
"narration": "Subscription renewal"
}
Sample Response
{
"status": 200,
"message": "Success",
"data": {
"authTypeRequired": null,
"paymentStatus": "SUCCESSFUL",
"onusReference": "ONUS-TKPY-2570434237835-03082025-103058",
"redirectUrl": null,
"message": "Card charged successfully",
"descriptor": null,
"processorRedirectHtmlOrUrl": null
}
}
Unlike the regular Card Payments response, authTypeRequired and redirectUrl are always null here — a tokenized charge never requires a completion step. Use the returned onusReference with Verify Single Payment to check status later, the same way you would for any other card payment.
Possible Errors
| Status | Meaning |
|---|---|
| 404 | Card token not found |
| 403 | Card token does not belong to the businessId supplied |
| 409 | reference has already been used for this business |
| 410 | Card token has been deleted (see Delete a Card Token) |
| 422 | Card token has expired |
| 422 | Business not profiled on the NGN card gateway (production only) |
Delete a Card Token
Permanently revoke a stored card. This deletes the token at the payment gateway as well as marking it deleted on our side — it can never be charged again afterward, and it stops appearing in Search Card Tokens results.
Request
- Path:
/api/v1/card-tokens/{id} - Method: DELETE
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string | Yes | The card token's ID (path parameter) |
| businessId | string | Yes | Your Business ID (query parameter) |
Sample Response
Calling delete again on a token you've already deleted is safe — it returns the same success shape ("message": "Card token already deleted") rather than an error, and does not attempt to contact the gateway a second time.
Possible Errors
| Status | Meaning |
|---|---|
| 404 | Card token not found |
| 403 | Card token does not belong to the businessId supplied |
Notes
- You never handle card data. Unlike a direct card payment integration, there's no encryption or card-capture form to build for this flow — the hosted consent page takes care of both.
- The environment (sandbox vs. production) is determined automatically by the
businessIdyou pass — there's nothing to configure on your end. - Consent links are single-use and expire (30 minutes by default, up to 24 hours). Plan your customer messaging accordingly — a link a customer opens after it has expired will show them a clear "link has expired" message and you'll need to generate a new one.
- Use the Testing Your Integration guide and the test cards already documented for Card Payments to complete a consent link end-to-end in sandbox.