# Webhooks

PayLynks POSTs events to your HTTPS endpoint as they happen.

## Register

```bash
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

```json
{ "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.

```ts
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`
