PayLynks

Webhooks

PayLynks POSTs events to your HTTPS endpoint as they happen.

Register

curl -X POST https://paylynks.io/api/v1/webhook_endpoints -H "Authorization: Bearer $PAYLYNKS_SECRET_KEY" \
  -H "Content-Type: application/json" -d '{"url": "https://your-app.com/api/paylynks/webhook"}'

The response includes secret (whsec_…) once. Store it as PAYLYNKS_WEBHOOK_SECRET. Or run npx paylynks webhooks add <url>, which does both.

  • event_types — default ["*"], or a list like ["payment_intent.succeeded"].
  • GET /api/v1/webhook_endpoints, DELETE /:id, POST /:id/test (sends a signed webhook_endpoint.test now and returns the status code).
  • URLs must be public https (no localhost or private addresses).

Payload

{ "id": "evt_...", "object": "event", "type": "payment_intent.succeeded", "livemode": false,
  "created": 1792540800, "data": { "object": { "id": "pi_...", "amount_total": 2575, "status": "succeeded" } } }

Verify the signature

Header: PayLynks-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>" with your secret>. Reject if the HMAC doesn't match or t is more than 5 minutes off. Use the raw request body.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(body: string, header: string | null, secret: string) {
  if (!header) return false;
  const p = Object.fromEntries(header.split(",").map((x) => x.split("=")));
  const t = Number(p.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest("hex");
  return expected.length === p.v1?.length && timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1));
}

Delivery

Respond with any 2xx within 8 seconds. Failures are retried after 1, 5 and 30 minutes, 2, 6 and 12 hours, then daily, for about three days. The same event can arrive more than once; use id to de-duplicate.

Event types

  • payment_intent.created, .succeeded, .processing, .payment_failed (bank payments are processing until they settle, up to 4 business days)
  • payment_link.created
  • invoice.created, .finalized, .sent, .payment_succeeded, .paid, .late_fee_applied, .voided
  • payment_request.created, .sent, .paid, .canceled, .expired
  • subscription.created, .activated, .payment_succeeded, .payment_failed, .unpaid, .completed, .paused, .resumed, .canceled, .payment_method_updated
  • refund.created, refund.failed
  • voice_session.created, .succeeded
  • donation_page.created
  • merchant.created, merchant.onboarding_completed (partners)
  • webhook_endpoint.test

View as markdown