Documentation Index
Fetch the complete documentation index at: https://docs.zafapay.com/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The recurring payments feature allows you to securely save customer card information and charge subsequent payments without requiring card input. Perfect for subscriptions and recurring billing.
Recurring payments are only available for certain gateways. Please contact us for availability.
If you use your own payment form with Server-to-Server (S2S) payments, you can also save cards by passing token, save_card: true, and customer_id together. See the S2S guide for details.
Recurring Payment Flow
Initial Payment (Save Card)
Save the card by specifying save_card: true and customer_id in the initial payment.
Request
curl -X POST https://api.sandbox.zafapay.com/v1/payments \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"amount": 10.00,
"currency": "usd",
"customer_id": "cust_abc123",
"save_card": true,
"external_id": "subscription_initial_001",
"metadata": {
"plan": "premium"
}
}'
A unique ID to identify the customer. Use your internal user ID.
Set to true for the initial recurring payment. The card will be saved after payment completion.
Response
{
"id": "req_abc123",
"status": "pending",
"amount": 10.00,
"currency": "usd",
"payment_type": "initial",
"payment_url": "https://pay.sandbox.zafapay.com/checkout/req_abc123?token=xxx",
"created_at": "2025-01-01T00:00:00.000Z"
}
payment_type: "initial" indicates this is an initial payment with card saving. The payment_method_id will be provided in the webhook after payment completion.
Redirect the customer to payment_url to complete the payment.
Webhook on Payment Completion
{
"event": "payment.succeeded",
"transaction_id": "tx_abc123",
"status": "succeeded",
"amount": "10.00",
"currency": "usd",
"external_id": "subscription_initial_001",
"payment_method": "card",
"payment_method_id": "pmi_xyz789",
"save_card": true,
"is_recurring": false,
"customer_id": "cust_abc123",
"card_brand": "visa",
"card_last4": "4242",
"card_country": "US",
"card_funding": "credit",
"metadata": { "plan": "premium" },
"created_at": "2025-01-01T00:00:00.000Z",
"timestamp": "2025-01-01T00:00:05.000Z"
}
Save the payment_method_id (pmi_xxx format) for subsequent payments.
List Saved Cards
Retrieve the customer’s saved cards.
curl https://api.sandbox.zafapay.com/v1/customers/cust_abc123/payment-methods \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response
{
"customer_id": "cust_abc123",
"payment_methods": [
{
"id": "pmi_xyz789",
"type": "card",
"card": {
"brand": "visa",
"last4": "4242",
"exp_month": 12,
"exp_year": 2028,
"cardholder_name": "John Doe",
"country": "US",
"funding": "credit"
},
"is_default": true,
"status": "active",
"created_at": "2025-01-01T00:00:00.000Z"
}
]
}
Execute Recurring Payment
Use the saved payment_method_id to charge without requiring card input.
Request
curl -X POST https://api.sandbox.zafapay.com/v1/payments \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"amount": 10.00,
"currency": "usd",
"customer_id": "cust_abc123",
"payment_method_id": "pmi_xyz789",
"external_id": "subscription_renewal_002",
"metadata": {
"plan": "premium",
"billing_cycle": 2
}
}'
The saved payment method ID (pmi_xxx format)
Response
Recurring payments are processed instantly (no payment_url is returned).
{
"id": "req_def456",
"transaction_id": "tx_def456",
"status": "succeeded",
"amount": 10.00,
"currency": "usd",
"payment_type": "recurring",
"created_at": "2025-02-01T00:00:00.000Z"
}
payment_type: "recurring" indicates this is a recurring payment using a saved card.
Webhook on Payment Success
{
"event": "payment.succeeded",
"transaction_id": "tx_def456",
"status": "succeeded",
"amount": "10.00",
"currency": "usd",
"external_id": "subscription_renewal_002",
"payment_method": "card",
"payment_method_id": "pmi_xyz789",
"is_recurring": true,
"customer_id": "cust_abc123",
"card_brand": "visa",
"card_last4": "4242",
"card_country": "US",
"card_funding": "credit",
"metadata": { "plan": "premium", "billing_cycle": 2 },
"created_at": "2025-02-01T00:00:00.000Z",
"timestamp": "2025-02-01T00:00:05.000Z"
}
Recurring payment webhooks include is_recurring: true.
Webhook on Payment Failure
When a recurring payment fails, a payment.failed event is sent. The error object contains details about the failure.
{
"event": "payment.failed",
"transaction_id": "tx_def456",
"status": "failed",
"amount": "10.00",
"currency": "usd",
"external_id": "subscription_renewal_002",
"payment_method": "card",
"payment_method_id": "pmi_xyz789",
"is_recurring": true,
"customer_id": "cust_abc123",
"card_brand": "visa",
"card_last4": "4242",
"error": {
"code": "card_declined",
"category": "soft_decline",
"message": "The card was declined",
"recommended_action": "use_different_card"
},
"created_at": "2025-02-01T00:00:00.000Z",
"timestamp": "2025-02-01T00:00:05.000Z"
}
Use error.recommended_action to determine the appropriate response:| Value | Description |
|---|
retry | Temporary error. Can retry after a delay |
contact_customer | Customer action needed (e.g., insufficient funds) |
use_different_card | A different card is required (e.g., expired card) |
none | No special action required |
Managing Payment Methods
Block a Card
Block a card if fraud is suspected.
curl -X POST https://api.sandbox.zafapay.com/v1/customers/payment-methods/pmi_xyz789/block \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"reason": "Suspected fraud"
}'
Unblock a Card
curl -X POST https://api.sandbox.zafapay.com/v1/customers/payment-methods/pmi_xyz789/unblock \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Delete a Card (Detach)
Use detach to permanently delete card information upon customer request.
curl -X POST https://api.sandbox.zafapay.com/v1/customers/payment-methods/pmi_xyz789/detach \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Response
{
"id": "pmi_xyz789",
"deleted": true
}
Deleted cards cannot be restored. To use again, the customer must re-register their card.
Block vs Delete:
- Block: Temporarily disable (fraud prevention). Can be unblocked later
- Delete (Detach): Permanently remove (GDPR compliance, customer deletion requests). Cannot be restored
Error Handling
Recurring Not Supported
{
"error": {
"type": "invalid_request_error",
"code": "recurring_not_supported",
"message": "Recurring payments are not supported for this connector",
"request_id": "req_xxx"
}
}
Card Expired
{
"error": {
"type": "payment_error",
"code": "expired_card",
"message": "The card has expired",
"request_id": "req_xxx"
}
}
If the card has expired or been declined by the issuer, the customer must register a new card.
Best Practices
Manage customer_id uniquely
Map your internal user ID 1:1 with customer_id. Using different customer_id values for the same customer will scatter their card information.
Persist payment_method_id
Store the payment_method_id received in the webhook to your database for subsequent payments.
Implement retry strategy
Card payments can fail due to temporary issues. Use exponential backoff for retries, and notify the customer after multiple failures.
Monitor expiration dates
Track saved card expiration dates and prompt customers to update their cards before expiration.