# 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

```http
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

```json
{
  "amount": 1299.99,
  "email": "customer@example.com",
  "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):**

```json
{
  "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

1. **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.

2. **Webhook notifications:** Bob Pay will send POST requests to your `notify_url` with payment status updates. Ensure your webhook endpoint is publicly accessible and can handle POST requests.

3. **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_url` option if you need to store or display a shorter URL
   - Include query parameters in your callback URLs to track which payment they relate to

4. **Custom payment ID:** Use the `custom_payment_id` field to link the Bob Pay payment to your internal order/transaction system. This ID will be included in webhook notifications.

#### Complete integration example

```javascript
// 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:

```json
{
  "amount": 1299.99,
  "email": "customer@example.com",
  "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

1. **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

2. **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.

3. **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.

4. **Multiple payment methods:** To allow customers to choose from all available payment methods, simply omit the `payment_method` parameter 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:**

```json
{
  "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": "customer@example.com",
  "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_code` field is always empty; your account code is available in `recipient_account.account_code`.
- A notification is also sent when a payment fails; the payload then has status `failed` and includes an `error_message` field.

#### 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:**

```javascript
// 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.

```javascript
// 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 `uuid` or `custom_payment_id` to prevent processing the same payment multiple times
- Store webhook receipts in your database to track which notifications you've already processed
- Return `200 OK` as 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

1. **Not receiving webhooks?**

   - Ensure your `notify_url` is 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

2. **Receiving duplicate webhooks?**

   - Implement idempotency using the `uuid` field
   - Check if your endpoint is returning non-200 status codes

3. **Signature validation failing?**

   - Use your own account code for `recipient_account_code` — the payload's top-level field is always empty (see `recipient_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
