Docs menu
Developers

Webhooks & API

Your backend talks to NetPay with signed JSON requests, and NetPay calls your backend with signed webhooks whenever money moves.

Before you start

  1. Create your keys

    Developers page, Create LIVE key / Create TEST key. Copy the secret when it is shown: it is shown once.

  2. Set your webhook URL

    Also on the Developers page. It has to be https and reachable from the internet.

  3. Keep secrets on your server

    Never put a key secret or the webhook secret in a browser, an app or a repository.

Signing requests

Every request carries three headers. The signature covers the timestamp, the method, the exact path with its query string, and the exact body bytes you send.

HeaderValue
X-Api-KeyYour key id.
X-TimestampCurrent Unix time in seconds. Requests more than 5 minutes off are refused.
X-SignatureHex HMAC-SHA256 with your key secret over timestamp + "\n" + METHOD + "\n" + path_and_query + "\n" + raw_body.
Idempotency-KeyOptional on POST, recommended. See below.
Sign the raw body

Sign the exact string you send, not a re-serialised copy of it. For GET the body is an empty string.

Node.jsJS
const crypto = require('crypto');

async function netpay(method, path, body) {
  const raw = body ? JSON.stringify(body) : '';
  const ts = String(Math.floor(Date.now() / 1000));
  const signature = crypto.createHmac('sha256', process.env.NETPAY_SECRET)
    .update(ts + '\n' + method + '\n' + path + '\n' + raw).digest('hex');
  const res = await fetch('https://api.netpay.ph' + path, {
    method, body: raw || undefined,
    headers: { 'Content-Type': 'application/json', 'X-Api-Key': process.env.NETPAY_KEY,
               'X-Timestamp': ts, 'X-Signature': signature },
  });
  return res.json();
}

Idempotency

Send Idempotency-Key on every POST; your order number is the natural fit. The same key with the same body returns the first response again (with Idempotent-Replayed: true), so retrying after a timeout never creates a second payin. The same key with a different body answers 409 IDEMPOTENCY_CONFLICT.

Responses and errors

Every response has the same envelope: {"status": "success", "data": {...}} or {"status": "failed", "error": "CODE", "message": "..."}, plus a request_id to quote when you contact us.

HTTPerrorMeaning
401AUTH_MISSING, AUTH_INVALID, AUTH_TIMESTAMPMissing headers, wrong key or signature, clock more than 5 minutes off.
403IP_NOT_ALLOWED, MERCHANT_INACTIVEYour IP allowlist or account status.
409DUPLICATE_ORDER, DUPLICATE_PAYOUT, IDEMPOTENCY_CONFLICT, INSUFFICIENT_BALANCEA repeat, or not enough available balance.
422VALIDATION_ERROR, AMOUNT_OUT_OF_RANGE, AMOUNT_BELOW_FEE, INVALID_CHANNEL, INVALID_BANK, CHANNEL_UNAVAILABLEFix the request; retrying the same one will not help.
429RATE_LIMITEDMore than 600 requests per minute.
502UPSTREAM_UNAVAILABLEThe processor did not accept the order. Retry later with the same Idempotency-Key.

Other endpoints

CallReturns
GET /v1/balanceAvailable, pending, frozen and total.
GET /v1/channelsChannels open to you now, with limits and your fee tiers.
GET /v1/banksPayout banks and e-wallets with their bank_code.
GET /v1/fees/quote?direction=PAYIN&channel=QRPH&amount=1500The fee you would pay right now.
GET /v1/pingSignature test.

Receiving webhooks

When a payin, payout or claim changes, NetPay posts to your webhook URL. Answer any 2xx within 10 seconds and do the slow work afterwards.

POST your webhook URLHTTP
POST https://yourshop.example/netpay/webhook
Content-Type: application/json
X-NetPay-Event: payin.succeeded
X-NetPay-Event-Id: evt_6a1f...
X-NetPay-Timestamp: 1762574400
X-NetPay-Signature: v1=<hex HMAC-SHA256 of timestamp + "." + raw body>

{
  "id": "evt_6a1f...",
  "type": "payin.succeeded",
  "created": "2026-11-08T12:01:00+08:00",
  "mode": "LIVE",
  "data": { "payin_no": "PI26110800AB12CD34EF", "merchant_order_id": "ORD-10042",
            "status": "SUCCEEDED", "amount": "1250.00", "fee": "25.00", "net_amount": "1225.00", ... }
}
EventSent when
payin.succeededA payin is paid.
payin.failed / payin.expired / payin.cancelledA payin ends without payment.
payout.succeededA payout is delivered.
payout.failed / payout.rejectedA payout fails or is declined; its hold is released.
claim.approved / claim.rejectedA settlement claim is decided.
Verify the signature before you trust it

Recompute the HMAC-SHA256 of the timestamp, a dot and the raw body with your webhook secret, and compare it to X-NetPay-Signature. Anyone can post to your URL; the signature is what makes it ours.

Verify a webhook (Node.js)JS
const crypto = require('crypto');

// rawBody: the exact bytes you received, before any JSON parsing
function isFromNetPay(req, rawBody) {
  const ts = req.headers['x-netpay-timestamp'] || '';
  const expected = 'v1=' + crypto.createHmac('sha256', process.env.NETPAY_WEBHOOK_SECRET)
    .update(ts + '.' + rawBody).digest('hex');
  const got = req.headers['x-netpay-signature'] || '';
  return got.length === expected.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}
Treat webhooks as at-least-once

Anything other than a 2xx is retried after 1 minute, 5 minutes, 15 minutes, 1 hour, 3, 6, 12 and 24 hours, and you can resend any delivery from Webhook Logs. Key your handler on the event id and make a repeat a no-op.

© 2026 NetPay.ph. All rights reserved.HomeSign in