Skip to content
SDK preview. Packages are not publicly published yet — use REST / cURL, or the local development artifacts.

Webhooks

Receive signed payment events on your server — set up, verify, deduplicate, and handle retries and ordering.

A webhook is an HTTP POST PayTaka sends to your server when a payment changes. It is the reliable way to learn that a payment was paid — even if the customer closes their browser.

Set up an endpoint#

  1. Developers → Webhooks → Add endpoint. Choose the environment (Test or Live), enter your HTTPS URL, and pick the events you want (or all of them).
  2. Copy the signing secret (whsec_…). It is shown once; rotate it any time from the endpoint page.
  3. Store it as PAYTAKA_WEBHOOK_SECRET on your server.

Production endpoints must be public https URLs. Use Send test event on a Test endpoint to check your receiver.

Test payments go to Test endpoints; live payments to Live endpoints#

PaymentDelivered to
Test payment (livemode: false)your Test webhook endpoint(s) only
Live payment (livemode: true)your Live webhook endpoint(s) only

Each endpoint has its own signing secret. A very common mistake is registering only a Live endpoint and wondering why test payments send nothing — register one for each environment you use, and use the secret of the endpoint that received the request.

Events#

TypeSent when
payment.createdA payment is created.
payment.paidA payment becomes PAID.
payment.cancelledA payment is cancelled.
payment.reversedA paid payment is reversed in PayTaka's records (not an automatic provider refund).
endpoint.testYou pressed Send test event (test endpoints only). Contains no Payment.

Webhooks describe hosted payments, the same objects /v1/payments returns. New event types may be added later: ignore types you don't recognise (the SDK gives you an unknown event you can log and skip).

JSON
{
  "id": "evt_2q8Zc0mRk1nT",
  "object": "event",
  "api_version": "v1",
  "type": "payment.paid",
  "livemode": false,
  "created_at": "2026-09-20T12:00:00Z",
  "data": { "object": { "id": "…", "status": "PAID", "amount": "500.00" } }
}

data.object is exactly what GET /v1/payments/{id} returns — including your external_id, merchant_reference and metadata, so you can match it to your order.

Verify the signature#

Every request carries PayTaka-Signature: t=<unix time>,v1=<hex>. The signature is HMAC-SHA256(secret, "<t>." + rawBody) over the exact bytes received — never a re-serialized JSON. Reject timestamps older than 5 minutes, and compare in constant time.

JavaScript
// Receive PayTaka webhooks with @paytaka/sdk (Node.js 18+, no framework).
import { createServer } from "node:http";
import { PayTakaSignatureVerificationError, verifyWebhook } from "@paytaka/sdk";

// Demo only: PayTaka delivers each event AT LEAST once, so remember the ids you have handled.
// In production use your database (a table with a unique event_id) instead of a Set.
const processed = new Set();

createServer(async (req, res) => {
  if (req.method !== "POST" || req.url !== "/paytaka/webhook") {
    res.writeHead(404).end();
    return;
  }

  // 1. Read the RAW body — do not JSON.parse before verifying.
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  const rawBody = Buffer.concat(chunks);

  // 2. Verify the signature. Never trust an unverified webhook.
  let event;
  try {
    event = verifyWebhook(rawBody, req.headers["paytaka-signature"], process.env.PAYTAKA_WEBHOOK_SECRET);
  } catch (error) {
    if (error instanceof PayTakaSignatureVerificationError) {
      res.writeHead(400).end();
      return;
    }
    throw error;
  }

  // 3. Already handled this event? Acknowledge and stop — never fulfil twice.
  if (processed.has(event.id)) {
    res.writeHead(200).end();
    return;
  }

  // 4. Fulfil ONLY from a verified payment.paid event. Ignore anything you don't recognise.
  if (event.type === "payment.paid") {
    console.log("Fulfil the order for payment", event.data.object.id);
  }
  processed.add(event.id);

  // 5. Any 2xx acknowledges the event. Don't rely on delivery order.
  res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({ received: true }));
}).listen(Number(process.env.PORT ?? 3000));

The Node.js, Java, Go and Python verifiers are automatically tested against the shared signature vectors. The PHP verifier implements the same algorithm and has not been run in automated tests yet.

Read the raw body#

Read the body before any JSON parsing, or the signature can't match. Framework notes:

  • Next.js — use verifyPayTakaWebhook(request) (it calls request.text()), or call request.text() yourself. Never call request.json() first: the body can only be read once.
  • Express — mount express.raw({ type: "application/json" }) for the webhook route, before any express.json().
  • Laravel — use $request->getContent(), not $request->all() or ->json(), and exclude the route from CSRF verification.
  • Spring Boot — take the body as @RequestBody String rawBody; parse it yourself after verifying, rather than binding to a DTO.

Handle events safely#

  1. Verify the signature (above).
  2. Deduplicate on event.id. If you have already processed that id, return 200 and do nothing.
  3. Fulfil only from a verified payment.paid — then mark the id processed.
Text
if event already processed → return 200
if event.type == "payment.paid" → fulfil the order for event.data.object
record event.id as processed → return 200

Keep processed ids in your database (a unique constraint on event.id makes this race-safe). Delivery is at least once, never exactly once.

Delivery and reliability#

  • Respond 2xx quickly to acknowledge. Redirects are not followed; 3xx, 4xx, 5xx and timeouts (10 s) count as failures.
  • Retries: failed deliveries are retried immediately, then after 30 s, 2 min, 10 min, 30 min, 2 h and 6 h. After that you can resend from the dashboard.
  • Duplicates happen. A retry sends the same id and the same body.
  • Do not rely on arrival order. payment.created may arrive after payment.paid. When your logic is sensitive to order, retrieve the payment (GET /v1/payments/{id}) and act on its current status rather than on the event alone.
  • An endpoint can be disabled at any time; it then receives nothing new. One that keeps failing is flagged Failing in the dashboard.

Test vectors#

To check your own verification code, webhook-signature-vectors.json in the repository lists deterministic (secret, timestamp, body) → signature cases. All PayTaka SDKs and examples must reproduce them byte for byte.