guides

Webhooks

apay pushes signed JSON events to your server for every payment, refund and dispute state change.

Configure

Set the URL in Dashboard → Developers → Webhooks, or on the business profile through the API:

curl -X POST https://apay.to/api/account/$MERCHANT_ID/business_profile/$PROFILE_ID \
  -H "api-key: $APAY_SECRET_KEY" -H "Content-Type: application/json" \
  -d '{ "webhook_details": { "webhook_url": "https://shop.example/apay/webhook",
        "webhook_version": "1.0.1",
        "payment_created_enabled": true, "payment_succeeded_enabled": true, "payment_failed_enabled": true } }'

Events

event_type When
payment_succeeded Funds captured
payment_failed Authorisation declined or errored
payment_processing Asynchronous method pending (bank transfer, some wallets)
payment_authorized Manual capture: authorised, awaiting capture
payment_captured Manual capture completed
payment_cancelled Voided before capture
action_required Customer action needed (3DS, redirect)
refund_succeeded, refund_failed Refund outcome
dispute_opened, dispute_challenged, dispute_won, dispute_lost Dispute lifecycle
mandate_active, mandate_revoked Saved payment method mandates
payout_success, payout_failed, … Payouts (when enabled)

Payload shape:

{
  "merchant_id": "merchant_1788365010",
  "event_id": "evt_…",
  "event_type": "payment_succeeded",
  "content": { "type": "payment_details", "object": { "payment_id": "pay_…", "status": "succeeded",
               "amount": 1999, "currency": "USD", "connector": "stripe_test", "…": "…" } },
  "timestamp": "2026-09-02T16:06:43.123Z"
}

Verify the signature

Every delivery carries x-webhook-signature-512: the hex HMAC-SHA512 of the raw request body, keyed with your merchant’s payment response hash key (Dashboard → Developers → Webhooks, or payment_response_hash_key on GET /accounts/{merchant_id}). Verify before acting.

Node.js:

const crypto = require("crypto");
app.post("/apay/webhook", express.raw({ type: "*/*" }), (req, res) => {
  const expected = crypto.createHmac("sha512", process.env.APAY_WEBHOOK_KEY).update(req.body).digest("hex");
  const given = req.get("x-webhook-signature-512") || "";
  if (expected.length !== given.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given)))
    return res.status(400).end();
  const event = JSON.parse(req.body);
  // idempotent: store event_id, then update your order by event.content.object.payment_id
  res.status(200).end();
});

Python:

import hmac, hashlib
def verify(raw_body: bytes, header: str, key: str) -> bool:
    expected = hmac.new(key.encode(), raw_body, hashlib.sha512).hexdigest()
    return hmac.compare_digest(expected, header)

Delivery

Sample receiver

The demo at apay.to/checkout uses a 120-line Node.js merchant backend that creates payments and receives webhooks: see app/merchant-demo/server.js in the apay repository, or ask dev@apay.to for a copy.