Complete integration guide for CheckoutPay Payment Gateway API. Build powerful payment solutions with our RESTful API.
Create an account and get your API key from the dashboard. Your API key is required for all authenticated requests.
Sign Up Nowhttps://camrentals.ng/api/v1
All API requests must:
X-API-Key header for authenticated endpoints (or send api_key in the JSON body for POST/PATCH only—prefer the header in production)Content-Type: application/json header
Use the correct HTTP method. For example, POST /api/v1/payment-request creates a payment. Opening that URL in a browser sends GET, which returns 405 Method Not Allowed (only POST is supported). Call the API from your server, Postman, or curl with POST.
Authenticated routes accept your API key in the X-API-Key header (recommended), or as api_key in the JSON body for POST/PATCH requests.
X-API-Key: pk_your_api_key_here
Security: Keep your API key secure and never expose it in client-side code. Store it securely on your server.
/payment-request
Create a new payment request. Returns account details for the customer to make payment. POST only—do not use GET (e.g. pasting the URL into a browser).
{
"amount": 5000.00,
"payer_name": "John Doe",
"bank": "GTBank",
"webhook_url": "https://yourwebsite.com/webhook/payment-status",
"service": "Product Purchase",
"transaction_id": "TXN-1234567890",
"business_website_id": 1,
"website_url": "https://yourwebsite.com"
}
| Parameter | Type | Required | Description |
|---|---|---|---|
| amount | decimal | Yes | Payment amount (minimum 0.01) |
| payer_name | string | Yes | Customer's name (required to get account number) |
| name | string | Yes* | Alternative to payer_name (either 'name' or 'payer_name' is required) |
| bank | string | No | Customer's bank name |
| fname | string | No | Customer first name |
| lname | string | No | Customer last name |
| bvn | string | No | BVN (if collected) |
| registration_number | string | No | Registration number (if applicable) |
| webhook_url | string | Yes | URL to receive payment notifications (must be from approved website) |
| service | string | No | Description of the service/product |
| transaction_id | string | No | Your unique transaction ID if provided; must not duplicate an existing payment |
| business_website_id | integer | No | ID of your approved website (for website-specific webhooks) |
| website_url | string | No | Your website URL (for website identification) |
{
"success": true,
"message": "Payment request created successfully",
"data": {
"transaction_id": "TXN-1234567890",
"amount": 5000.00,
"payer_name": "John Doe",
"account_number": "0123456789",
"account_name": "Your Business Name",
"bank_name": "GTBank",
"status": "pending",
"expires_at": "2024-01-15T12:00:00Z",
"created_at": "2024-01-15T10:00:00Z",
"charges": {
"percentage": 50.00,
"fixed": 50.00,
"total": 100.00,
"paid_by_customer": false,
"amount_to_pay": 5000.00,
"business_receives": 4900.00
},
"website": {
"id": 1,
"url": "https://yourwebsite.com"
}
}
}
/payment/{transactionId}/amount
Correct the expected amount for a pending payment. Use this when your site sent the wrong amount (e.g. customer paid a different sum). The system updates the transaction amount, recalculates charges, and immediately re-runs email matching so any bank alert with the actual amount paid can be matched and the payment approved.
When to use: Only pending, non-expired payments can be updated.
Recommended flow: Call PATCH /payment/{transactionId}/amount with the correct amount, then poll GET /payment/{transactionId} until status changes, or rely on your webhook for final confirmation. The webhook sent when a payment is approved is unchanged (same payment.approved payload) when the payment was matched after an amount correction.
{
"new_amount": 7500.00
}
{
"success": true,
"message": "Transaction amount successfully updated. Recalculated charges and matching initiated.",
"data": {
"transaction_id": "TXN-1234567890",
"amount": 7500.00,
"payer_name": "John Doe",
"bank": "GTBank",
"account_number": "0123456789",
"account_name": "Your Business Name",
"bank_name": "GTBank",
"status": "pending",
"webhook_url": "https://yourwebsite.com/webhook/payment-status",
"expires_at": "2024-01-15T12:00:00Z",
"matched_at": null,
"approved_at": null,
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:15:00Z",
"charges": { "percentage": 50.00, "fixed": 50.00, "total": 100.00, "paid_by_customer": false, "business_receives": 7400.00 },
"website": { "id": 1, "url": "https://yourwebsite.com" }
}
}
/payment/{transactionId}
Retrieve payment details by transaction ID. Use this to poll for status after creating a payment or after correcting the amount with PATCH. Response structure is stable; new fields may be added.
{
"success": true,
"data": {
"transaction_id": "TXN-1234567890",
"amount": 5000.00,
"payer_name": "John Doe",
"bank": "GTBank",
"account_number": "0123456789",
"account_name": "Your Business Name",
"bank_name": "GTBank",
"status": "approved",
"webhook_url": "https://yourwebsite.com/webhook/payment-status",
"expires_at": "2024-01-15T12:00:00Z",
"matched_at": "2024-01-15T10:30:00Z",
"approved_at": "2024-01-15T10:35:00Z",
"created_at": "2024-01-15T10:00:00Z",
"updated_at": "2024-01-15T10:35:00Z",
"charges": {
"percentage": 50.00,
"fixed": 50.00,
"total": 100.00,
"paid_by_customer": false,
"business_receives": 4900.00
},
"website": {
"id": 1,
"url": "https://yourwebsite.com"
}
}
}
/payments
List all payments for your business with optional filters.
| Parameter | Type | Description |
|---|---|---|
| status | string | Filter by status: pending, approved, rejected |
| from_date | date | Filter payments from this date (YYYY-MM-DD) |
| to_date | date | Filter payments until this date (YYYY-MM-DD) |
| website_id | integer | Filter by website ID |
| per_page | integer | Number of results per page (default: 15) |
GET https://camrentals.ng/api/v1/payments?status=approved&from_date=2024-01-01&per_page=20
X-API-Key: pk_your_api_key_here
Lets your server use the same X-API-Key as bank-transfer payments. Checkout must enable WhatsApp wallet API on your business (admin). Nigerian wallet numbers only. Throttle: 30 requests/minute on this group (in addition to global API limits).
/whatsapp-wallet/lookup
JSON body: phone. Returns balance, wallet_id, has_pin, tier.
/whatsapp-wallet/ensure
JSON body: phone. Creates a Tier-1 wallet row if missing.
/whatsapp-wallet/send-message
JSON body: phone, message (max 4000). Sends your composed plain text via WhatsApp (e.g. OTP). Same X-API-Key as other wallet routes — no extra Checkout env secret per merchant.
/whatsapp-wallet/pay/start
Recommended: Your backend sends order_summary and amount; Checkout sends the customer a WhatsApp with a secure PIN link. On success, credits your business and POSTs payment.approved to webhook_url (must match your saved business or approved website webhook URL).
POST https://camrentals.ng/api/v1/whatsapp-wallet/pay/start
Content-Type: application/json
X-API-Key: pk_your_api_key_here
{
"phone": "08012345678",
"amount": 2500.00,
"order_reference": "ORDER-TRACK-123",
"order_summary": "2x Jollof rice\n1x Zobo\nDelivery: Surulere",
"payer_name": "Ada Customer",
"webhook_url": "https://your-app.com/api/webhooks/checkout/payment",
"idempotency_key": "order-123-wallet-try-1"
}
Response data.confirm_url is the same URL messaged to the customer. Link TTL from env WHATSAPP_WALLET_PARTNER_PAY_INTENT_TTL_MINUTES (default 30).
No PIN-less debit endpoint. Merchant debits always require the customer to confirm on the Checkout PIN page after pay/start (WhatsApp link).
Webhooks allow you to receive real-time notifications when payment events occur. You can set webhook URLs at the business level or per-website level for more granular control.
Webhooks are sent in the following priority order:
When a payment is approved, you'll receive a POST request to your webhook URL with the following payload (structure is stable; new fields may be added in the future). This includes payments that were matched after an amount correction via PATCH /payment/{transactionId}/amount—the webhook payload is unchanged.
{
"event": "payment.approved",
"transaction_id": "TXN-1234567890",
"external_reference": "ORDER-TRACK-123",
"status": "approved",
"amount": 5000.00,
"received_amount": 5000.00,
"payer_name": "John Doe",
"bank": "GTBank",
"payer_account_number": "0123456789",
"account_number": "0987654321",
"is_mismatch": false,
"mismatch_reason": null,
"charges": {
"percentage": 50.00,
"fixed": 50.00,
"total": 100.00,
"business_receives": 4900.00
},
"timestamp": "2024-01-15T10:35:00Z",
"email_data": {}
}
Fields: event, transaction_id, external_reference (when set on the payment, e.g. WhatsApp wallet pay/start order_reference), status, amount (requested), received_amount (actual received; use for reconciliation), payer_name, bank, payer_account_number, account_number (your account), is_mismatch, mismatch_reason, charges, timestamp, email_data (optional raw email info).
Automatic Charges Mismatch Detection: Our system automatically detects when a customer pays the base amount without including charges.
If the following conditions are met, the payment will be automatically approved with a mismatch flag:
In this case, the webhook will include:
is_mismatch: truereceived_amount - The actual amount received (base amount without charges)mismatch_reason - Explanation of the mismatchamount - The originally requested amount (includes charges){
"event": "payment.approved",
"transaction_id": "TXN-1234567890",
"status": "approved",
"amount": 2070.00, // Requested amount (includes charges)
"received_amount": 2000.00, // Actual amount received (base amount)
"is_mismatch": true,
"mismatch_reason": "Customer paid base amount without charges. Expected: ₦2,070.00, Received: ₦2,000.00 (charges: ₦70.00)",
"name_mismatch": false,
"charges": {
"percentage": 20.00,
"fixed": 50.00,
"total": 70.00,
"paid_by_customer": false,
"business_receives": 1930.00 // received_amount - charges
},
...
}
Important: When handling charges mismatch, your business balance will be credited with the business_receives amount (received_amount minus charges), not the full requested amount. Always check is_mismatch and received_amount fields in your webhook handler to process payments correctly.
Important: Always validate webhook requests on your server. Consider implementing:
You can set webhook URLs in two ways:
Note: Webhook URLs must be from your approved website domains. Add and approve websites in your dashboard before using them.
$apiKey = 'pk_your_api_key_here';
$apiUrl = 'https://camrentals.ng/api/v1';
$data = [
'amount' => 5000.00,
'payer_name' => 'John Doe', // Required
'bank' => 'GTBank',
'webhook_url' => 'https://yourwebsite.com/webhook/payment-status',
'service' => 'Product Purchase'
];
$ch = curl_init($apiUrl . '/payment-request');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: ' . $apiKey
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$result = json_decode($response, true);
if ($httpCode === 201 && $result['success']) {
echo "Payment created: " . $result['data']['transaction_id'];
echo "Account Number: " . $result['data']['account_number'];
} else {
echo "Error: " . $result['message'];
}
const apiKey = 'pk_your_api_key_here';
const apiUrl = 'https://camrentals.ng/api/v1';
const createPayment = async () => {
const response = await fetch(`${apiUrl}/payment-request`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey
},
body: JSON.stringify({
amount: 5000.00,
payer_name: 'John Doe',
bank: 'GTBank',
webhook_url: 'https://yourwebsite.com/webhook/payment-status',
service: 'Product Purchase'
})
});
const result = await response.json();
if (result.success) {
console.log('Payment created:', result.data.transaction_id);
console.log('Account Number:', result.data.account_number);
} else {
console.error('Error:', result.message);
}
};
createPayment();
import requests
import json
api_key = 'pk_your_api_key_here'
api_url = 'https://camrentals.ng/api/v1'
data = {
'amount': 5000.00,
'payer_name': 'John Doe', # Required
'bank': 'GTBank',
'webhook_url': 'https://yourwebsite.com/webhook/payment-status',
'service': 'Product Purchase'
}
headers = {
'Content-Type': 'application/json',
'X-API-Key': api_key
}
response = requests.post(
f'{api_url}/payment-request',
headers=headers,
data=json.dumps(data)
)
result = response.json()
if response.status_code == 201 and result['success']:
print(f"Payment created: {result['data']['transaction_id']}")
print(f"Account Number: {result['data']['account_number']}")
else:
print(f"Error: {result['message']}")
<?php
// webhook-handler.php
$payload = json_decode(file_get_contents('php://input'), true);
if ($payload['event'] === 'payment.approved') {
$transactionId = $payload['transaction_id'];
$amount = $payload['amount'];
$status = $payload['status'];
// Update your database
// Mark order as paid, send confirmation email, etc.
// Always return 200 OK to acknowledge receipt
http_response_code(200);
echo json_encode(['status' => 'received']);
} else {
http_response_code(400);
echo json_encode(['error' => 'Unknown event']);
}
?>
Application-level errors (API key, webhook domain, not found) usually return success: false and a message string. Laravel validation errors (HTTP 422) return message (often the first problem found) and an errors object keyed by field—inspect errors in your client.
{
"success": false,
"message": "Error description here"
}
| Code | Meaning |
|---|---|
| 200 | Success |
| 201 | Created (payment request created) |
| 400 | Bad Request (e.g. webhook domain not approved) |
| 401 | Unauthorized (invalid, inactive, or missing API key) |
| 404 | Not Found |
| 405 | Method Not Allowed (wrong HTTP verb, e.g. GET on POST-only routes) |
| 422 | Unprocessable Entity (Laravel validation errors; response includes errors by field) |
| 429 | Too Many Requests (rate limit exceeded) |
| 500 | Internal Server Error |
Status: 401
{
"success": false,
"message": "Invalid or inactive API key"
}
Status: 401
{
"success": false,
"message": "API key is required"
}
Status: 422 (validation)
{
"message": "The payer name is required to get an account number. Please provide either \"name\" or \"payer_name\".",
"errors": {
"payer_name": [
"The payer name is required to get an account number. Please provide either \"name\" or \"payer_name\"."
]
}
}
Exact wording may vary slightly; always inspect the errors object.
Status: 400
{
"success": false,
"message": "Webhook URL must be from your approved website domain."
}
Status: 400
{
"success": false,
"message": "Insufficient balance",
"available_balance": 5000.00
}
The api middleware applies Laravel’s default API rate limiter: 60 requests per minute, keyed by authenticated user id when the request is authenticated, otherwise by IP address.
If you exceed rate limits, you'll receive a 429 Too Many Requests response. Implement exponential backoff for retries.
Get support from our team or check out additional resources.