Skip to main content

Testing webhooks

Local testing with a public tunnel

Your local server is not reachable from the internet, so PAI cannot deliver webhooks to localhost. Use a tunneling tool to expose your local port via a public HTTPS URL.

Using ngrok

  1. Install ngrok: ngrok.com/download

  2. Start your local server (e.g. on port 3000).

  3. Open a tunnel:

    ngrok http 3000
  4. ngrok prints a public HTTPS URL:

    Forwarding https://abc123.ngrok.io -> http://localhost:3000
  5. Register that URL as a webhook destination in PAI:

    curl -X POST "https://staging-api.payments.ai/v1/webhook-destinations" \
    -H "Authorization: ApiKey ${API_KEY}" \
    -H "Content-Type: application/json" \
    -d '{
    "url": "https://abc123.ngrok.io/webhooks",
    "auth": {
    "type": "basic",
    "username": "user",
    "password": "pass"
    }
    }'
  6. Trigger an action in PAI (e.g. create a transaction) and watch events arrive in your local server logs and in the ngrok inspector at http://localhost:4040.

The ngrok URL changes every time you restart ngrok. Update the webhook destination URL in PAI whenever you restart.


Common errors

Wrong signature

Symptom: signature verification rejects every incoming request.

Cause: HMAC computed on the parsed/re-serialized JSON body instead of the raw bytes.

Fix: read the raw request body before any JSON parsing:

Node.js (Express)
app.use(express.raw({ type: 'application/json' }));

app.post('/webhooks', (req, res) => {
const rawBody = req.body; // Buffer, not a parsed object
const signature = req.headers['x-pai-signature'];
if (!verifySignature(rawBody, signature, process.env.WEBHOOK_SECRET)) {
return res.sendStatus(401);
}
const payload = JSON.parse(rawBody);
// …
});

PAI keeps retrying even though the event was processed

Cause: the endpoint returned 201, 204, or a redirect instead of exactly 200.

Fix: explicitly return 200:

res.status(200).send();

Handler processes the same event multiple times

Cause: PAI retried delivery because an earlier request timed out.

Fix: deduplicate on deduplicationId before processing. See Best practices.