TRADOM PaymentDocs

Receive payment events

Register webhooks, verify signatures, process events idempotently, and recover missed events

TRADOM Payment sends PaymentIntent state changes to your registered URLs. Delivery is at least once, so deduplicate events by event ID.

Register an endpoint

Use a secret key to register a webhook endpoint.

const endpoint = await paymentIntentClient.webhookEndpoints.create({
  url: "https://shop.example/webhooks/tradom-payment",
});

if (!endpoint.secret) {
  throw new Error("Webhook secret was not returned");
}

await saveWebhookSecret(endpoint.secret);

The whsec_… value is returned only when the endpoint is created. Save it immediately in a secure secret store. The response from list() does not include it.

The receiving URL must be a publicly reachable HTTP(S) URL. Private and loopback addresses are rejected as an SSRF precaution.

Verify the signature

Verify the raw request body. A string re-serialized from parsed JSON will not produce the same signature.

import {
  constructEvent,
  PaymentError,
} from "@tradom/payment";

const TRADOM_PAYMENT_WEBHOOK_SECRET =
  process.env.TRADOM_PAYMENT_WEBHOOK_SECRET;

if (!TRADOM_PAYMENT_WEBHOOK_SECRET) {
  throw new Error("TRADOM_PAYMENT_WEBHOOK_SECRET is required");
}

export async function POST(request: Request) {
  try {
    const event = constructEvent({
      payload: await request.text(),
      signature: request.headers.get("Tradom-Signature") ?? "",
      secret: TRADOM_PAYMENT_WEBHOOK_SECRET,
    });

    if (await isProcessed(event.id)) {
      return new Response("ok");
    }

    if (event.type === "payment_intent.succeeded") {
      await fulfilOrder(event.data.object.metadata);
    }

    await markProcessed(event.id);
    return new Response("ok");
  } catch (error) {
    if (error instanceof PaymentError) {
      return new Response("invalid webhook", { status: 400 });
    }
    throw error;
  }
}

Signature format:

Tradom-Signature: t=<unix>,v1=<hex hmac-sha256(secret, "<t>.<raw-body>")>
Tradom-Event-Id: evt_…

The SDK allows a timestamp difference of 300 seconds by default and compares signatures in constant time.

Event types

The current production release sends these events:

  • payment_intent.succeeded
  • payment_intent.canceled
  • payment_intent.refunded

It does not send payment_intent.created, refund.created, or refund.updated.

Retries

Delivery is attempted once synchronously when the event occurs; on failure a cron redelivers every 10 minutes for up to 72 hours. There are at most 5 attempts in total (initial + 4 retries), with no back-off. If only some endpoints fail, an event may also be sent again to an endpoint that already succeeded.

Each attempt times out after 5 seconds; only a 2xx counts as success, and 3xx redirects are not followed (they count as failures). Always deduplicate with event.id or Tradom-Event-Id, return a 2xx as soon as you have received the event, and do heavy processing asynchronously — a handler slower than 5 seconds causes a retry and a duplicate delivery.

There is currently no API to rotate a signing secret or to disable/delete an endpoint. If a secret leaks, register a new endpoint and retire the old URL on your side.

Recover missed events

Use the Events API if your endpoint misses a webhook:

const events = await paymentIntentClient.events.list();
const event = await paymentIntentClient.events.retrieve("evt_...");

list() returns up to 100 events, newest first. The current API does not support pagination parameters.

On this page