Skip to content

ADB Payment Webhook API Documentation

Overview

AEON Bank (ADB) sends Tulus the zakat payments its customers make in the AEON Bank app, through one webhook. Tulus files each payment under the right zakat board and shows it in the board's reports.

Each call carries a JSON object with an array of transactions. Every transaction is handled on its own: in a call of three, two can be saved and one not.

Updated 12 November 2026 to match how the server actually behaves. If you built against the earlier version of this page, note in particular:

  • You do not choose the agency. Tulus files every payment itself from the order number and data; the agency field is ignored (see How Tulus files a payment).
  • A wrong or missing secret answers 400, not 401.
  • There is no schema validation, so most mistakes are accepted with 200. Treat only {"status":"ok"} as accepted, and check the payment appears in the reports (see Response Format).
  • Sending the same orderno again with a different status or amount replaces the stored payment.

Endpoint Details

Staging (testing)Production (live)
WebhookPOST https://staging.tulus.my/api/adb/webhookPOST https://pay.tulus.my/api/adb/webhook
Sync (credentials check)POST https://staging.tulus.my/api/adb/syncPOST https://pay.tulus.my/api/adb/sync
Reportsreports-staging.tulus.myreports.tulus.my

Every request needs Content-Type: application/json. Each environment has its own two secrets and its own reports login; Tulus sends them to you separately, never in a document. Test on Staging first. Staging's data is never settled.

Authentication

EndpointHeaderSecret
WebhookX-API-Password (X-Notification-Secret is also accepted)Webhook secret
SyncX-Notification-Secret onlySync secret, a different value from the webhook's

A wrong or missing secret, or the wrong header for the endpoint, answers 400 {"status":"invalid secret"}. The sync endpoint only checks the secret and stores nothing, so it is a safe way to confirm your credentials:

curl -s -w ' %{http_code}\n' -X POST https://staging.tulus.my/api/adb/sync \
  -H "Content-Type: application/json" \
  -H "X-Notification-Secret: YOUR_SYNC_SECRET" -d '{}'
# {"status":"ok"} 200

Request Body Schema

The body is {"transactions": [ ... ]}. Field names are matched without regard to case; order_no (with an underscore) is not the same as orderno and is ignored.

Transaction Object

FieldTypeRequiredDescription
ordernostring Yes Your unique order number. It identifies the payment: sending it again updates the same payment. It can also decide the zakat board (see below). Without it the payment is not stored, although the answer is still 200.
amountnumber or numeric string Yes Amount in RM, stored as sent. Without it the payment is not stored, although the answer is still 200.
statusstring Yes success or failed, in lower case (SUCCESS, failure and FAILURE are also understood). Any other value is stored as pending, including pending and mixed case such as Success or Failed.
timestring Yes When the payment was made, ISO 8601 with the Malaysia offset, e.g. 2026-11-12T18:02:00+08:00 (seconds optional). Always send +08:00; other offsets are not supported. A missing or unreadable time is not refused; the time Tulus received the call is used instead.
namestring Yes Payer's name, as shown to the zakat board.
emailstring Yes Payer's email.
dataobject Yes Zakat details (see Data Object). zakat_type and zakat_state decide where the payment is filed.
categorystring No Stored as the product description, e.g. zakat.
agencystring No Ignored. Send ADB. Tulus files the payment itself; sending ADBLZS or ADBPPZ here changes nothing.

Data Object

Stored with the payment and shown to the zakat board. Tulus reads three things from it: the zakat type, the state, and the payer's IC.

FieldTypeDescription
zakat_typestringDecides fitrah or not: a value containing fitrah (e.g. fitrah, zakat_fitrah) is zakat fitrah; anything else is another zakat (harta). Values in use are listed below.
zakat_statestringUsed to choose Selangor or Wilayah Persekutuan when the order number does not name the board (see below).
zakat_ic_nostringPayer's MyKad number, 12 digits. A payment without an IC is still stored, with a missing-IC warning for the board.
zakat_namestringName for the zakat payment.
emailstringEmail for the zakat payment.
zakat_yearstringYear of the zakat, 4 digits, e.g. "2026".
zakat_phonestringPhone number, digits only.
zakat_address, zakat_postcode, zakat_citystringPayer's address.
zakat_amountstringZakat amount, e.g. "14.00".
zakat_rate, zakat_dependantstringZakat fitrah only: rice price per person and number of people (tanggungan). amount should equal rate × dependants.

zakat_type values in use: fitrah, zakat_fitrah, zakat_emas, zakat_kwsp, zakat_padi, zakat_pendapatan, zakat_perak, zakat_perniagaan, zakat_pertanian, zakat_qada, zakat_rikaz, zakat_saham, zakat_simpanan, zakat_ternakan.

zakat_state values in use: johor, kedah, kelantan, melaka, negeri_sembilan, pahang, pulau_pinang, perak, perlis, sabah, sarawak, selangor, terengganu, kuala_lumpur, labuan, putrajaya.

How Tulus files a payment

First match wins:

  1. Fitrah or not: data.zakat_type containing fitrah.
  2. Board:
    1. ADBPNG or ADBJHR anywhere in the orderno (any case): Pulau Pinang or Johor. This is the only way to file a payment under Pulau Pinang or Johor; zakat_state is not used for them.
    2. LZS or PPZ anywhere in the orderno: Selangor or Wilayah Persekutuan.
    3. data.zakat_state: selangor; or kuala_lumpur, putrajaya, labuan (Wilayah Persekutuan).
    4. None of these: the payment is held under ADB for Tulus to review.
BoardZakat fitrahOther zakat
Selangor (LZS)ADBFLZSADBLZS
Wilayah Persekutuan (PPZ)ADBFPPZADBPPZ
Pulau PinangADBFPNGADBPNG
JohorADBFJHRADBJHR

Put the board the payer chose into the orderno (ADBLZS, ADBPPZ, ADBPNG, ADBJHR). zakat_state is the payer's home state, which can differ from the board they pay: a Selangor resident paying Pulau Pinang must be filed under Pulau Pinang. Matching is by substring, so do not let those letters appear in an order number by accident.

Status and updates

  • The orderno identifies the payment. Sending it again with the same status and amount changes nothing, so retrying is safe.
  • Sending it again with a different status or amount replaces the stored payment. Use this to report a pending payment's final result; avoid it otherwise, since even a success can be overwritten.

Example Request

The examples use Staging and made-up payers. For Production, use https://pay.tulus.my and your Production secret.

Single Transaction (other zakat, Selangor)

curl -s -w ' %{http_code}\n' -X POST https://staging.tulus.my/api/adb/webhook \
  -H "Content-Type: application/json" \
  -H "X-API-Password: YOUR_WEBHOOK_SECRET" \
  -d '{
    "transactions": [
      {
        "agency": "ADB",
        "orderno": "TEST-ADBLZS-0001",
        "amount": 112.34,
        "name": "Ujian Pembayar",
        "email": "[email protected]",
        "time": "2026-11-12T10:10:10+08:00",
        "status": "success",
        "category": "zakat",
        "data": {
          "zakat_type": "zakat_pendapatan",
          "zakat_state": "selangor",
          "zakat_ic_no": "000000000001",
          "zakat_name": "Ujian Pembayar",
          "zakat_year": "2026",
          "zakat_amount": "112.34"
        }
      }
    ]
  }'
# {"status":"ok"} 200  -> filed under ADBLZS

Multiple Transactions (fitrah to PPZ, and Pulau Pinang)

curl -s -w ' %{http_code}\n' -X POST https://staging.tulus.my/api/adb/webhook \
  -H "Content-Type: application/json" \
  -H "X-API-Password: YOUR_WEBHOOK_SECRET" \
  -d '{
    "transactions": [
      {
        "agency": "ADB",
        "orderno": "TEST-ADBPPZ-0002",
        "amount": 14,
        "name": "Ujian Pembayar Dua",
        "email": "[email protected]",
        "time": "2026-11-12T11:10:11+08:00",
        "status": "success",
        "category": "zakat",
        "data": {
          "zakat_type": "zakat_fitrah",
          "zakat_state": "kuala_lumpur",
          "zakat_ic_no": "000000000002",
          "zakat_rate": "7",
          "zakat_dependant": "2",
          "zakat_amount": "14.00"
        }
      },
      {
        "agency": "ADB",
        "orderno": "TEST-ADBPNG-0003",
        "amount": 12.34,
        "name": "Ujian Pembayar Tiga",
        "email": "[email protected]",
        "time": "2026-11-12T11:15:30+08:00",
        "status": "success",
        "category": "zakat",
        "data": {
          "zakat_type": "zakat_simpanan",
          "zakat_state": "selangor",
          "zakat_ic_no": "000000000003"
        }
      }
    ]
  }'
# {"status":"ok"} 200  -> ADBFPPZ and ADBPNG (the order number beats the Selangor state)

Pending, then the final result

# 1. pending
... "orderno": "TEST-ADBLZS-0004", "status": "pending", ...
# 2. later, the same orderno with the result: replaces the pending payment
... "orderno": "TEST-ADBLZS-0004", "status": "success", ...

Response Format

HTTPBodyMeaningWhat to do
200{"status":"ok"}Every transaction was passed on to the reports.Done. A transaction missing orderno or amount also gets this answer but is not stored, so always send both.
200Error saving session (plain text, not JSON)The body could not be read (bad JSON, missing Content-Type, empty transactions), or at least one transaction could not be saved.Check the body, not just the status. Fix the request if it was malformed; otherwise retry later with the same body (safe, see above). For a well-formed body Tulus also keeps a copy and retries within about 10 minutes.
400{"status":"invalid secret"}Wrong or missing secret, or the wrong header for this endpoint.Check the header name and which secret you used.

There is no 401, no 409, and no 400 for a bad payload.

Important Notes

  1. Send the board in the order number, especially for Pulau Pinang and Johor (see above).
  2. Unique order numbers: reusing an orderno for a different payment replaces the first one.
  3. Status: lower case. Success or Failed with a capital letter is stored as pending.
  4. Time: send +08:00; a missing or unreadable time becomes the time Tulus received the call.
  5. Amounts: there is no minimum and no decimal-place check; what you send is what the board sees.
  6. No validation: email, IC, phone and postcode formats are not checked. Wrong values are stored as sent and reach the zakat board.

Testing

  • On Staging, start every orderno with TEST- and use made-up payers (example.test emails, all-zero ICs as above). TEST- rows are highlighted in the reports and no receipt email is sent for them.
  • On Production, a live test must not be mixed with real collections: use a payer email at @aeonbank.com.my whose name contains test (e.g. [email protected]). Tulus files it under a separate test code (X + the board's code). Make test amounts end in 12.34 so anyone reconciling can spot them.
  • Check each test payment in the reports with the login Tulus gave you: set the date range to the day you sent it and look for the order number.

Support

For technical support or credentials, contact the Tulus development team at [email protected].

Schema Reference

A JSON schema describing what to send is at https://developers.tulus.my/schema/adb.json. It documents the contract; the server does not validate requests against it.