Generating a payment URL
To direct your customer to the Bob Pay payment screen, you will need to create a payment URL and then navigate to that URL.
- Sandbox environment:
https://sandbox.bobpay.co.za - Production environment:
https://my.bobpay.co.za
Create the payment URL
Endpoint: POST /payments/intents/link
Authentication: Required - Bearer token (a JWT obtained via /login, or your API key sent as the bearer token)
Description: Creates a payment intent and returns a unique payment URL. Your application should navigate to this URL to direct your customer to the Bob Pay payment page, where they can complete their payment using their preferred payment method.
Request headers
Content-Type: application/json
Authorization: Bearer YOUR_API_TOKEN
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
amount |
float | Yes | The payment amount in ZAR. Must be greater than zero. |
email |
string | Conditional* | Customer's email address. |
mobile_number |
string | Conditional* | Customer's mobile number in international format (e.g., +27821234567). |
item_name |
string | No | Name of the product or service being purchased. |
item_description |
string | No | Detailed description of the product or service. |
custom_payment_id |
string | No | Your unique reference/identifier for this payment. If not provided, a UUID will be generated automatically. |
notify_url |
string | No* | Webhook URL where payment status notifications will be sent. |
success_url |
string | No* | URL to redirect the customer after a successful payment. |
pending_url |
string | No* | URL to redirect the customer when the payment is pending. |
cancel_url |
string | No* | URL to redirect the customer if they cancel the payment. |
short_url |
boolean | No | If set to true, a shortened URL will also be generated and returned. Default: false. |
* Note: Either email or mobile_number must be provided. The callback URLs are optional, but strongly recommended: without a notify_url you will not receive payment notifications, and without the redirect URLs your customer will not be returned to your site after payment.
Example request
{
"amount": 1299.99,
"email": "[email protected]",
"mobile_number": "+27821234567",
"item_name": "Wireless Bluetooth Headphones",
"item_description": "Premium noise-cancelling wireless headphones - Black",
"custom_payment_id": "ORDER-2024-001",
"notify_url": "https://yourwebsite.co.za/webhook/payment-notification",
"success_url": "https://yourwebsite.co.za/payment/success",
"pending_url": "https://yourwebsite.co.za/payment/pending",
"cancel_url": "https://yourwebsite.co.za/payment/cancelled",
"short_url": true
}
Response
Success response (200 OK):
{
"url": "https://sandbox.bobpay.co.za/pay/ref/3WFFG",
"short_url": "https://api.sandbox.bob.co.za/r/V4X7WG"
}
| Field | Type | Description |
|---|---|---|
url |
string | The full payment URL to redirect your customer to. |
short_url |
string | The shortened payment URL. Equal to url unless short_url: true was requested. |
Usage notes
-
Navigation flow: After receiving the payment URL in the response, your application should immediately redirect/navigate the user's browser to this URL. This typically happens during your checkout process.
-
Webhook notifications: Bob Pay will send POST requests to your
notify_urlwith payment status updates. Ensure your webhook endpoint is publicly accessible and can handle POST requests. -
URL best practices:
- All callback URLs (notify_url, success_url, pending_url, cancel_url) must be valid URLs; we strongly recommend HTTPS
- Use the
short_urloption if you need to store or display a shorter URL - Include query parameters in your callback URLs to track which payment they relate to
-
Custom payment ID: Use the
custom_payment_idfield to link the Bob Pay payment to your internal order/transaction system. This ID will be included in webhook notifications.
Complete integration example
// Example using Node.js and Express
const axios = require("axios");
const express = require("express");
const app = express();
app.post("/checkout", async (req, res) => {
try {
const orderDetails = req.body;
// Create the payment link
const response = await axios.post(
"https://api.sandbox.bobpay.co.za/v2/payments/intents/link",
{
amount: orderDetails.totalAmount,
email: orderDetails.customerEmail,
mobile_number: orderDetails.customerMobile,
item_name: orderDetails.productName,
item_description: orderDetails.productDescription,
custom_payment_id: orderDetails.orderId,
notify_url: "https://yourwebsite.com/webhook/payment",
success_url: `https://yourwebsite.com/order/${orderDetails.orderId}/success`,
pending_url: `https://yourwebsite.com/order/${orderDetails.orderId}/pending`,
cancel_url: `https://yourwebsite.com/order/${orderDetails.orderId}/cancelled`,
short_url: false,
},
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.BOBPAY_API_TOKEN}`,
},
}
);
// Redirect the customer to Bob Pay payment page
const paymentUrl = response.data.url;
res.redirect(paymentUrl);
} catch (error) {
console.error(
"Error creating payment link:",
error.response?.data || error.message
);
res.status(500).send("Payment initialisation failed");
}
});
Payment methods
By default, when a customer visits the payment URL, they are presented with all available payment methods enabled for your merchant account. However, you can direct customers to a specific payment method by specifying the payment_method in the request body.
Include the payment_method parameter when creating the payment link:
{
"amount": 1299.99,
"email": "[email protected]",
"item_name": "Wireless Bluetooth Headphones",
"notify_url": "https://yourwebsite.co.za/webhook/payment-notification",
"success_url": "https://yourwebsite.co.za/payment/success",
"pending_url": "https://yourwebsite.co.za/payment/pending",
"cancel_url": "https://yourwebsite.co.za/payment/cancelled",
"payment_method": "credit-card"
}
Available payment methods
| Value | Description |
|---|---|
account-balance |
Pay using Bob Pay account balance |
credit-card |
Credit or debit card payment (Visa, Mastercard, Amex, Diners) |
apple-pay |
Apple Pay |
google-pay |
Google Pay |
instant-eft |
Real-time bank transfer via Instant EFT |
manual-eft |
Upload proof of payment for manual EFT transfer |
pay-shap |
PayShap instant payment method |
capitec-pay |
Capitec Pay instant payment method |
scan-to-pay |
Scan to Pay (QR code payment) |
nedbank-direct-eft |
Nedbank Direct EFT instant payment method |
absa-pay |
Absa Pay instant payment method |
Important notes
-
Payment method availability: Not all payment methods may be available for your account. Payment methods are subject to:
- Global payment method activation status
- Merchant account-specific settings
- Transaction amount limits per payment method
- Account status and permissions
-
Transaction limits: Each payment method has a maximum transaction amount. If the payment amount exceeds the limit for a specified payment method, that method will be shown as inactive.
-
Fallback behaviour: If you specify a payment method that is not available or inactive, the customer will see it as disabled with an appropriate message.
-
Multiple payment methods: To allow customers to choose from all available payment methods, simply omit the
payment_methodparameter from the URL.
Handling notifications
When a payment is completed, Bob Pay sends a POST notification to the notify_url you specified when creating the payment intent. This webhook notification contains the complete payment details.
Webhook request
Method: POST
Content-Type: application/json
Request body example:
{
"id": 12345,
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"short_reference": "3J9QZ",
"custom_payment_id": "ORDER-2024-001",
"amount": 1299.99,
"paid_amount": 1299.99,
"total_paid_amount": 1299.99,
"status": "paid",
"payment_method": "instant-eft",
"original_requested_payment_method": "instant-eft",
"payment_id": 67890,
"payment": {
"id": 67890,
"payment_method_id": 3,
"payment_method": "instant-eft",
"amount": 1299.99,
"status": "success"
},
"item_name": "Wireless Bluetooth Headphones",
"item_description": "Premium noise-cancelling wireless headphones - Black",
"recipient_account_code": "",
"recipient_account_id": 100,
"recipient_account": {
"id": 100,
"name": "Your Store",
"account_code": "ABC123",
"signup_name": "Your Store",
"is_merchant": true
},
"email": "[email protected]",
"mobile_number": "+27821234567",
"from_bank": "FNB",
"time_created": "2024-01-15T10:30:00Z",
"is_test": false,
"signature": "5d41402abc4b2a76b9719d911017c592",
"notify_url": "https://yourwebsite.co.za/webhook/payment-notification",
"success_url": "https://yourwebsite.co.za/payment/success",
"pending_url": "https://yourwebsite.co.za/payment/pending",
"cancel_url": "https://yourwebsite.co.za/payment/cancelled"
}
The webhook URL will also include a query parameter: ?type=payment
Notes:
- The payload may contain additional fields not shown in the example above.
- The top-level
recipient_account_codefield is always empty; your account code is available inrecipient_account.account_code. - A notification is also sent when a payment fails; the payload then has status
failedand includes anerror_messagefield.
Security & validation
To ensure the webhook is legitimate and the payment is valid, you must perform the following security checks:
1. Verify source IP address
All webhook requests originate from Bob Pay's static IP addresses. Verify that the request comes from one of these IPs:
- Sandbox environment:
13.245.58.93 - Production environment:
13.246.100.25
2. Verify the signature
The signature field in the webhook payload is an MD5 hash that you should verify to confirm the data hasn't been tampered with.
Signature calculation:
// Example: Node.js signature verification
const crypto = require("crypto");
// URL-encode a value the same way Bob Pay does (form encoding: spaces become "+")
function queryEscape(value) {
return encodeURIComponent(value)
.replace(/%20/g, "+")
.replace(/[!'()*]/g, (c) => "%" + c.charCodeAt(0).toString(16).toUpperCase());
}
function verifySignature(webhookData, accountPassphrase, accountCode) {
// The top-level recipient_account_code field in the payload is always empty.
// The signature is calculated over your actual account code, so use your own
// account code (also available in webhookData.recipient_account.account_code).
const keyValuePairs = [
`recipient_account_code=${queryEscape(accountCode)}`,
`custom_payment_id=${queryEscape(webhookData.custom_payment_id)}`,
`email=${queryEscape(webhookData.email || "")}`,
`mobile_number=${queryEscape(webhookData.mobile_number || "")}`,
`amount=${webhookData.amount.toFixed(2)}`,
`item_name=${queryEscape(webhookData.item_name || "")}`,
`item_description=${queryEscape(webhookData.item_description || "")}`,
`notify_url=${queryEscape(webhookData.notify_url)}`,
`success_url=${queryEscape(webhookData.success_url)}`,
`pending_url=${queryEscape(webhookData.pending_url)}`,
`cancel_url=${queryEscape(webhookData.cancel_url)}`,
];
const signatureString =
keyValuePairs.join("&") + `&passphrase=${accountPassphrase}`;
const calculatedSignature = crypto
.createHash("md5")
.update(signatureString)
.digest("hex");
return calculatedSignature === webhookData.signature;
}
Note: The account passphrase is a secret key associated with your merchant account. This passphrase can be found in your account settings.
3. Verify payment with Bob Pay
After verifying the signature, you should confirm the payment's validity with Bob Pay by calling the validation endpoint with the full webhook payload.
Endpoint: POST /payments/intents/validate
Request body: Send the complete webhook payload you received.
Success response (200 OK): The payment is valid and confirmed.
Error response: A non-200 response is returned when the payment could not be verified or doesn't match.
4. Verify the amount
Ensure the paid_amount matches the expected amount for your order. Note that some payment methods may allow non-exact payments if enabled for your account.
Responding to webhooks
Important: You must return a 200 OK HTTP status code to acknowledge receipt of the webhook. If Bob Pay doesn't receive a 200 OK response, it will retry sending the webhook.
// Example: Express.js webhook endpoint
app.post("/webhook/payment-notification", async (req, res) => {
try {
const webhookData = req.body;
// 1. Verify source IP
const clientIp = req.ip || req.connection.remoteAddress;
const allowedIPs = ["13.245.58.93", "13.246.100.25"]; // Sandbox and Production
if (!allowedIPs.includes(clientIp)) {
return res.status(403).send("Forbidden");
}
// 2. Verify signature
const isValidSignature = verifySignature(
webhookData,
YOUR_PASSPHRASE,
YOUR_ACCOUNT_CODE
);
if (!isValidSignature) {
return res.status(400).send("Invalid signature");
}
// 3. Verify amount
const expectedAmount = await getOrderAmount(webhookData.custom_payment_id);
if (webhookData.paid_amount !== expectedAmount) {
return res.status(400).send("Amount mismatch");
}
// 4. Validate with Bob Pay (send full webhook payload)
const validationResponse = await axios.post(
"https://api.sandbox.bobpay.co.za/v2/payments/intents/validate",
webhookData,
{
headers: {
Authorization: `Bearer ${process.env.BOBPAY_API_TOKEN}`,
"Content-Type": "application/json",
},
}
);
// 5. Process the payment in your system
await processOrder(webhookData.custom_payment_id, webhookData);
// 6. Return 200 OK to acknowledge receipt
res.status(200).send("OK");
} catch (error) {
console.error("Webhook processing error:", error);
// Still return 200 if you've successfully received and logged the webhook
// to prevent retries for processing errors
res.status(200).send("Received");
}
});
Webhook retry mechanism
If Bob Pay doesn't receive a 200 OK response from your webhook endpoint, it will automatically retry sending the notification using an exponential backoff strategy:
Retry schedule: Bob Pay retries with increasing delays, up to 17 retries over approximately 4 days.
Best practices:
- Make your webhook endpoint idempotent (able to handle duplicate notifications safely)
- Use the
uuidorcustom_payment_idto prevent processing the same payment multiple times - Store webhook receipts in your database to track which notifications you've already processed
- Return
200 OKas quickly as possible; perform time-consuming operations asynchronously
Payment statuses
| Status | Description |
|---|---|
unpaid |
Payment intent created but not yet paid |
paid |
Payment completed successfully |
failed |
Payment failed or was declined |
canceled |
Payment was cancelled by the customer or system |
refund_pending |
A refund has been initiated but not yet completed |
refunded |
Payment has been fully refunded |
partially_refunded |
Payment has been partially refunded |
chargeback |
A chargeback was raised against the payment |
deleted |
Payment intent was deleted |
Troubleshooting
-
Not receiving webhooks?
- Ensure your
notify_urlis publicly accessible (not localhost) - Verify your server accepts POST requests
- Check your firewall allows traffic from Bob Pay's IP addresses
- Ensure your endpoint returns a 200 OK status code
- Ensure your
-
Receiving duplicate webhooks?
- Implement idempotency using the
uuidfield - Check if your endpoint is returning non-200 status codes
- Implement idempotency using the
-
Signature validation failing?
- Use your own account code for
recipient_account_code— the payload's top-level field is always empty (seerecipient_account.account_code) - Ensure you're using the correct passphrase
- Verify you're URL-encoding values correctly (spaces must be encoded as
+, not%20) - Check that you're using the exact fields in the correct order
- Ensure you're formatting the amount to 2 decimal places
- Use your own account code for