Welcome to the AffanPay API documentation
You can now start developing code to communicate with AffanPay. This documentation provides detailed explanations on how to use each API endpoint effectively. All AffanPay APIs return responses in JSON format, including error messages.
API endpoint:
https://app.affanpay.my/api

Token

Every request to the AffanPay API must carry a token in its Authorization header. You generate that token from your AffanPay account.

How to get your token
  1. Log in to your AffanPay account.
  2. Go to Settings → Developer.
  3. Under API tokens, give the token a name — for example Production store — then click Generate token.
  4. Copy it straight away. The token is shown once and cannot be retrieved again.

POST Create Bill https://app.affanpay.my/api/v1/bill

This API is used to create a bill. The generated bill can be directly used to initiate and receive payment collections.

Authorization
Token : generated from Settings → Developer
Request header
Accept: application/json
category
string
(Optional) Category ID for the bill. This can be obtained in the Category section after logging into your account.
name
string
(Required) The name or title of the bill.
description
string
(Optional) A short description of the bill (maximum 200 characters).
html
string
(Optional) HTML content for the bill’s description. Must contain valid HTML tags.
amount
decimal
(Required) The total amount for the bill. Accepts up to 2 decimal places. Minimum value is 5.00.
fee_charge_payer
boolean
(Optional) Indicates whether the payer will bear the transaction fee. Value true/false
redirect_url
string
(Optional) URL to redirect the customer after payment completion.
callback_url
string
(Optional) Endpoint to receive asynchronous payment status updates (webhook).
external_ref
string
(Required) External reference or unique identifier for the bill in your system.
customer_name
string
(Required) Full name of the customer. Maximum 60 characters.
customer_email
(Required) Customer’s email address. Maximum 50 characters.
customer_phone
string
(Optional) Customer’s phone number. Maximum 20 characters.
source
(Optional) Source identifier for the bill creation. Maximum 40 characters.
expiry_date
string
(Optional) Expiry date of the bill. Must be today or later. Format: YYYY-MM-DD HH:MM:SS.
Example Code

    <?php

    $curl = curl_init();

    curl_setopt_array($curl, array(
        CURLOPT_URL => 'https://app.affanpay.my/api/v1/bill',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING => '',
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT => 0,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST => 'POST',
        CURLOPT_POSTFIELDS =>'{            
            "name": "",
            "amount": "",
            "customer_name": "",
            "customer_email": ""            
        }',
        CURLOPT_HTTPHEADER => array(
            'Accept: application/json',
            'Content-Type: application/json',
            'Authorization: Bearer [token from Settings > Developer]'
        ),
    ));

    $response = curl_exec($curl);

    curl_close($curl);
    echo $response;

                
Example Response

    {
        "success": true,
        "id": "pujsSUNLeIYo",
        "url": "https://app.affanpay.my/pujsSUNLeIYo"
    }
                    

GET Requery Payment https://app.affanpay.my/api/v1/requery?query=value

AffanPay sends a payment status callback to your system for every transaction, whether successful or not. However, in some cases, the callback may fail or not be received as intended. This API allows you to query the latest status of a payment transaction directly from AffanPay’s system.

Authorization
Token : generated from Settings → Developer
Request header
Accept: application/json
Query param
To customise the request, you can append query parameters to the endpoint URL. Two types of parameters are supported but only one can be used at a time.
bill_id
The unique identifier of the bill used to retrieve its details and related payment records.
e.g: bill_id=wxUyT47ko3v
payment_ref
The reference number of the payment used to retrieve its transaction details.
e.g: payment_ref=AP01384088585413050521
Example Code

    <?php

    $curl = curl_init();

    curl_setopt_array($curl, array(
        CURLOPT_URL => 'https://app.affanpay.my/api/v1/requery?bill_id=wxUyT47ko3v',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_ENCODING => '',
        CURLOPT_MAXREDIRS => 10,
        CURLOPT_TIMEOUT => 0,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST => 'GET',
        CURLOPT_HTTPHEADER => array(
            'Accept: application/json',
            'Authorization: Bearer [token from Settings > Developer]'
        ),
    ));

    $response = curl_exec($curl);

    curl_close($curl);
    echo $response;

                
Example Response

    {
        "data": {
            "id": "wxUyT47ko3v",
            "name": "Multi open amount",
            "payments": [
                {
                    "payment_ref": "AP01384263015313050521",
                    "status_code": 4,
                    "status": "No Action",
                    "nett_amount": " 0.00",
                    "payment_channel": null
                },
                {
                    "payment_ref": "AP01384355115313050521",
                    "status_code": 3,
                    "status": "Failed",
                    "nett_amount": " 0.00",
                    "payment_channel": "fpx"
                },
                {
                    "payment_ref": "AP01384088585413050521",
                    "status_code": 1,
                    "status": "Success",
                    "nett_amount": " 31.20",
                    "payment_channel": "fpx"
                }
            ]
        }
    }
                    

POST Callback to your callback_url

When a payment reaches a final state, AffanPay sends a POST request to the callback_url you supplied when creating the bill. This is how your system learns that an order has been paid.

Request headers we send
Content-Type: application/json
X-AFFANPAY-TIMESTAMP: 1756684800
X-AFFANPAY-HMAC: f10f01be8675bfaa…
What we expect back
Answer HTTP 200 once you have processed the callback. The body is not read. Any status of 400 or above is treated as a failure and recorded against the transaction.
Body (json)
{
    "payment_ref": "AP01384355115313050521",
    "status_code": 1,
    "status": "Success",
    "paid_amount": "324.76",
    "payer": {
        "name": "Ahmad bin Ali",
        "phone": "0123456789",
        "email": "ahmad@example.my"
    },
    "payment_channel": "fpx",
    "bank_reference": "2609011012340000",
    "bank_response_datetime": "2026-09-01 10:12:34",
    "bill": {
        "id": "k3jh2x",
        "name": "Invoice 12/2026",
        "amount": "320.75",
        "external_ref": "ORDER-1001",
        "created_at": "2026-09-01 09:00:00"
    }
}
Fields
Field Description
payment_refOur transaction reference. Unique per payment attempt — use it as your idempotency key.
status_code1 Success · 3 Failed. (2 Pending and 4 No Action exist but never trigger a callback.) Decide from this field, never from status.
statusHuman-readable label for the code above. For display only.
paid_amountWhat the payer actually paid. If the bill passes the service charge to the payer this is higher than bill.amount. Cast it before comparing — do not compare it as a string.
payerName, phone and email as entered by the payer. Any of them can be null.
payment_channelThe channel used, e.g. fpx, duitnow_qr, grabpay. Can be null.
bank_referenceReference returned by the bank or wallet. Can be null.
bill.idThe bill code, the same value that appears in the payment page URL.
bill.amountThe amount the bill was created for, before any service charge.
bill.external_refYour own reference, exactly as you sent it when creating the bill. null if you did not send one.

Verifying the signature

Every callback carries an X-AFFANPAY-HMAC header so you can prove the request came from us and not from someone who guessed your callback URL. Verifying is optional — an integration that ignores these headers keeps working exactly as before — but it is strongly recommended, because your callback URL is reachable by anyone.

The signature is a lowercase hex SHA-256 HMAC of timestamp + "." + raw request body, keyed with your secret.

$secret    = 'YOUR_SECRET_KEY';           // Settings -> Developer
$body      = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_AFFANPAY_TIMESTAMP'] ?? '';
$received  = $_SERVER['HTTP_X_AFFANPAY_HMAC'] ?? '';

$expected = hash_hmac('sha256', $timestamp . '.' . $body, $secret);

// hash_equals, not ===, so a wrong signature cannot be found by timing
if (!hash_equals($expected, $received)) {
    http_response_code(400);
    exit('invalid signature');
}

// reject a callback that was captured and replayed later
if (abs(time() - (int) $timestamp) > 300) {
    http_response_code(400);
    exit('stale callback');
}

$payload = json_decode($body, true);

if ((int) $payload['status_code'] === 1) {
    // mark your order paid here - safely, this may run twice
}

http_response_code(200);