Authentication
Create a key in the dashboard under API keys. Keys start with ph_live_ and are shown once. Send the key in the X-API-Key header (or as Authorization: Bearer ph_live_…). All requests go to:
https://paidhook.com/v1
Call the API from your server only. Never put an API key in a web page or mobile app.
Check a payment
GET /mutations/check: did a payment of this exact amount arrive recently? This is the call most shops use to approve an order.
curl -H "X-API-Key: ph_live_…" \
"https://paidhook.com/v1/mutations/check?amount=50000&refresh=true&wait=90"
| Parameter | Description |
|---|---|
amount required | Exact amount to look for, e.g. 50000. |
type | CR (money in, default) or DB (money out). |
within_minutes | Only rows PaidHook received in this window. Default 1440, max 10080. |
bank | Destination bank only, e.g. bca, bri or myBCA. |
account_number | Destination account number only. |
device_id | One connected account only. |
refresh | true asks the connected bank for a fresh check first. For an account read by PaidHook Operator, the phone is read right away. |
wait | With refresh: seconds to wait for that check, up to 120. |
Response:
{
"found": true,
"matches": [ { "id": 1, "type": "CR", "amount": 50000.0, "date": "2026-10-29", … } ],
"last_sync_at": "2026-10-29T04:12:08Z",
"requests": [ { "id": 7, "status": "done", … } ],
"refresh_skipped": []
}
List transactions
GET /mutations: transactions, newest first. Filter with type, date_from/date_to (YYYY-MM-DD), min_amount/max_amount, q (text in the bank description), received_after and device_id. Page with limit (max 200) and pass next_before_id as before_id for older rows.
curl -H "X-API-Key: ph_live_…" "https://paidhook.com/v1/mutations?type=CR&limit=20"
Each transaction includes the bank description split into fields in details: counterparty_name, counterparty_bank, channel, transfer_ref, note and the original lines.
Connected accounts
GET /devices: your connected bank accounts and whether each is running. status is one of active, needs_bank_login, paused, stopped or offline.
curl -H "X-API-Key: ph_live_…" "https://paidhook.com/v1/devices"
Fresh bank check
POST /devices/{id}/sync asks one connected account for a fresh check, at most once every 2 minutes per account. Follow it with GET /sync-requests/{id} until status is done or failed. A refused request returns 409 with the reason: offline, not_active, pending or cooldown.
Webhooks
Add an HTTPS endpoint under API keys → Webhooks (up to 5). PaidHook sends a POST with this JSON body for each new transaction. Reply with any 2xx status within 10 seconds.
{
"id": "evt_8f2c…",
"type": "transaction.created",
"created_at": "2026-11-02T11:08:40Z",
"data": {
"id": 1, "device_id": 2, "institution": "KlikBCA", "account_number": "8201299047",
"date": "2026-10-29", "type": "CR", "amount": 10000.0, "balance": 20000.0,
"description": "BI-FAST CR TRANSFER DR 542 …",
"details": { "kind": "transfer", "counterparty_bank": "Bank Jago", … }
}
}
- Headers:
Paidhook-Event,Paidhook-Event-IdandPaidhook-Signature. - Failed deliveries are retried after 30 s, 2 min, 10 min, 1 h, 6 h and 16 h, then marked failed. The
idstays the same, so use it to ignore repeats. - The dashboard can send a
pingtest event to check your endpoint.
Verify signatures
Paidhook-Signature is t=<unix seconds>,v1=<hex HMAC-SHA256>, computed over "<t>." + raw body with your endpoint secret (whsec_…). Recompute it over the raw body and reject old timestamps.
PHP
$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_PAIDHOOK_SIGNATURE']), $sig);
$expected = hash_hmac('sha256', $sig['t'] . '.' . $body, 'whsec_…');
if (!hash_equals($expected, $sig['v1']) || abs(time() - (int) $sig['t']) > 300) {
http_response_code(400); exit;
}
$event = json_decode($body, true); // mark the order as paid here
http_response_code(200);
Python
import hmac, hashlib, time
parts = dict(p.split("=", 1) for p in request.headers["Paidhook-Signature"].split(","))
expected = hmac.new(b"whsec_…", f"{parts['t']}.".encode() + request.body, hashlib.sha256).hexdigest()
assert hmac.compare_digest(expected, parts["v1"]) and abs(time.time() - int(parts["t"])) < 300
Errors
| Status | Meaning |
|---|---|
401 | Missing, invalid or revoked API key. |
404 | No connected account matches device_id, bank or account_number. |
409 | A fresh check was refused (see Fresh bank check). |
422 | A parameter has the wrong format. |
The complete, always-current list of fields is in the API reference.