Skip to main content
Webhooks let AbabilPay push event notifications to your server as things happen — no polling required. When a payment is confirmed, an invoice goes overdue, or a bridge transfer completes, AbabilPay sends an HTTP POST to your registered endpoint with a signed JSON payload.

Registering a webhook endpoint

Register an endpoint from Dashboard → Settings → Webhooks, or via API:
POST https://testnetv1.ababilpay.xyz/api/v1/webhooks
Authorization: Bearer sk_test_...
Content-Type: application/json

{
  "url":    "https://yourserver.com/webhooks/ababilpay",
  "events": ["payment.completed", "invoice.paid", "refund.issued"]
}

Response

{
  "success": true,
  "data": {
    "webhook_id": "wh_a1b2c3d4",
    "url":        "https://yourserver.com/webhooks/ababilpay",
    "events":     ["payment.completed", "invoice.paid", "refund.issued"],
    "secret":     "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "created_at": "2026-05-25T10:00:00Z"
  }
}
Store the secret immediately — it is shown only once. You will use it to verify every incoming webhook signature.

Payload format

Every webhook request is a POST with Content-Type: application/json. The body follows a consistent envelope:
{
  "id":         "evt_9f3e2a1b",
  "event":      "payment.completed",
  "created_at": "2026-05-25T12:05:00Z",
  "data": {
    "intent_id":     "d03e0751-5a79-4b33-aa0e-d33dee344f27",
    "status":        "paid",
    "amount_usdc":   29.99,
    "order_id":      "order_8821",
    "tx_hash":       "0xf4753b209212ece119cb099f8f499efb6a4df031...",
    "buyer_address": "0xBuyerAddress",
    "chain":         "eip155:84532",
    "paid_at":       "2026-05-25T12:05:00Z"
  }
}
FieldTypeDescription
idstringUnique event ID. Use this to deduplicate retries.
eventstringEvent type. See Event types below.
created_atstringISO 8601 timestamp of when the event was generated.
dataobjectEvent-specific payload. Shape varies by event type.

Verifying signatures

Every request includes an AbabilPay-Signature header. Always verify this before processing the payload — it proves the request came from AbabilPay and the body was not tampered with. The signature is an HMAC-SHA256 of the raw request body, signed with your webhook secret.
import crypto from "crypto"

function verifyWebhook(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)          // rawBody must be the raw Buffer, not parsed JSON
    .digest("hex")

  const trusted = Buffer.from(`sha256=${expected}`, "utf8")
  const received = Buffer.from(signatureHeader, "utf8")

  // Constant-time comparison prevents timing attacks
  if (trusted.length !== received.length) return false
  return crypto.timingSafeEqual(trusted, received)
}

// Express.js example
app.post("/webhooks/ababilpay", express.raw({ type: "application/json" }), (req, res) => {
  const sig = req.headers["ababilpay-signature"]

  if (!verifyWebhook(req.body, sig, process.env.ABABILPAY_WEBHOOK_SECRET)) {
    return res.status(401).send("Invalid signature")
  }

  const event = JSON.parse(req.body)

  switch (event.event) {
    case "payment.completed":
      // fulfil order using event.data.order_id
      break
    case "invoice.paid":
      // mark invoice settled
      break
  }

  res.status(200).send("ok")
})
import hmac
import hashlib
import os
from flask import Flask, request, abort

app = Flask(__name__)

def verify_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

@app.route("/webhooks/ababilpay", methods=["POST"])
def webhook():
    sig = request.headers.get("AbabilPay-Signature", "")
    raw = request.get_data()

    if not verify_webhook(raw, sig, os.environ["ABABILPAY_WEBHOOK_SECRET"]):
        abort(401)

    event = request.get_json(force=True)

    if event["event"] == "payment.completed":
        pass  # fulfil order using event["data"]["order_id"]
    elif event["event"] == "invoice.paid":
        pass  # mark invoice settled

    return "ok", 200
Use the raw request body for signature verification — not a re-serialised version of the parsed JSON. Any whitespace difference will cause verification to fail.

Event types

EventTriggerKey fields in data
payment.completedPayment intent fully settled on-chain.intent_id, tx_hash, amount_usdc, order_id, chain, paid_at
payment.failedPayment attempt failed before settlement.intent_id, reason, order_id
payment.expiredIntent was not paid within the 30-minute window.intent_id, order_id, expired_at
invoice.paidInvoice fully settled.invoice_id, amount_usdc, paid_at
invoice.overdueInvoice is past its due date with an unpaid balance.invoice_id, amount_usdc, due_date
refund.issuedRefund processed and USDC returned to the buyer.refund_id, amount_usdc, tx_hash
bridge.completedCCTP bridge transfer settled on the destination chain.bridge_id, from_chain, to_chain, amount_usdc, tx_hash
transfer.completedUSDC transfer to an external wallet confirmed.transfer_id, to_address, amount_usdc, tx_hash
payroll.batch_completedAll recipients in a payroll batch have been paid.batch_id, total_usdc, recipient_count
fraud.flaggedAI agent flagged a transaction for review.transaction_id, risk_score, reason

Retry behavior

If your endpoint does not return a 2xx response within 10 seconds, AbabilPay retries with exponential backoff:
AttemptDelay after previous
1Immediate
21 minute
35 minutes
430 minutes
52 hours
After 5 failed attempts the event is marked as failed and no further retries occur. You can manually replay failed events from Dashboard → Settings → Webhooks. Deduplication: Use the id field to detect retries. Store processed event IDs and skip any event whose ID you have already handled.

Testing webhooks

Use the sandbox environment to send test events without real on-chain transactions:
POST https://testnetv1.ababilpay.xyz/api/v1/webhooks/test
Authorization: Bearer sk_test_...
Content-Type: application/json

{
  "webhook_id": "wh_a1b2c3d4",
  "event":      "payment.completed"
}
AbabilPay will deliver a simulated payment.completed payload to your registered endpoint so you can test your handler end-to-end. For local development, use a tunneling tool such as ngrok to expose your local server:
ngrok http 3000
# Copy the https:// URL → register it as your webhook endpoint in the dashboard