Webhook endpoints

A webhook endpoint is a URL Plugipay POSTs events to when something interesting happens in your workspace — a payment succeeds, a subscription renews, a refund settles. This page documents the API for managing those endpoints programmatically: registering new ones, updating subscribed event types, rotating signing secrets, inspecting recent deliveries.

For the event payload format, the full event-type catalog, and the signature verification recipe, see API → Webhooks. For the dashboard walkthrough, see Portal → Webhooks.

You don't need this resource to receive webhooks. Most integrators add endpoints in the dashboard once and never touch the API. The endpoints below exist for partners provisioning customer workspaces, infra-as-code setups, and processor migrations.

The webhook endpoint object

{
  "id": "whep_01HXxxxxxxxxxxxxxxxxxxxxxx",
  "url": "https://api.example.com/plugipay/webhooks",
  "events": ["payment.succeeded", "refund.succeeded"],
  "description": "Production handler",
  "status": "active",
  "signingSecret": "whsec_a1b2c3d4...",
  "createdAt": "2026-05-12T10:42:00.123Z",
  "updatedAt": "2026-05-12T10:42:00.123Z",
  "lastDelivery": {
    "eventId": "evt_01HZxxxxxxxxxxxxxxxxxxxxxx",
    "deliveredAt": "2026-05-12T10:55:01.811Z",
    "statusCode": 200,
    "durationMs": 142
  }
}
Field Type Notes
id string Prefix whep_. Stable identifier; safe to log.
url string The HTTPS URL events POST to. http:// allowed in test mode only.
events string[] Event types this endpoint subscribes to. ["*"] means "everything".
description string | null Free-form label for your own bookkeeping.
status enum active, disabled (manually), or errored (auto-disabled, see below).
signingSecret string | null The HMAC secret. Only present on create and rotate-secret responses. null otherwise.
createdAt ISO 8601
updatedAt ISO 8601 Bumped on update, rotate, or status change.
lastDelivery object | null Summary of the most recent delivery attempt. null until the first event fires.

signingSecret is shown only on create and rotate-secret. Every other endpoint returns it as null. Store it in your secret manager immediately — there's no recovery flow.

Create an endpoint

POST /v1/webhook-endpoints

Request body

Field Type Required Notes
url string yes Must start with https:// in live mode (http:// accepted in test mode). Private IPs and localhost are rejected; use the CLI tunnel for local dev.
events string[] no Event types to subscribe to. Omit or pass ["*"] for all events. Unknown event types return validation_error.
description string no Free-form label, max 200 chars.
enabled boolean no Defaults to true. Pass false to create the endpoint paused.

Response

201 Created with the webhook endpoint object. The signingSecret field is populated.

Node

const endpoint = await plugipay.webhookEndpoints.create({
  url: 'https://api.example.com/plugipay/webhooks',
  events: ['payment.succeeded', 'refund.succeeded'],
  description: 'Production handler',
});

await secretStore.set('PLUGIPAY_WEBHOOK_SECRET', endpoint.signingSecret);

Python

endpoint = plugipay.webhook_endpoints.create(
    url="https://api.example.com/plugipay/webhooks",
    events=["payment.succeeded", "refund.succeeded"],
    description="Production handler",
)
secret_store.set("PLUGIPAY_WEBHOOK_SECRET", endpoint.signing_secret)

Go

ep, err := client.WebhookEndpoints.Create(ctx, &plugipay.WebhookEndpointCreate{
    URL:         "https://api.example.com/plugipay/webhooks",
    Events:      []string{"payment.succeeded", "refund.succeeded"},
    Description: plugipay.String("Production handler"),
})

curl

plugipay_curl POST '/v1/webhook-endpoints' '{
  "url": "https://api.example.com/plugipay/webhooks",
  "events": ["payment.succeeded", "refund.succeeded"],
  "description": "Production handler"
}'

Errors

Code Status When
validation_error 400 url missing or not a valid http(s) URL; an event type isn't in the catalog.
url_not_allowed 400 localhost, RFC1918, or http:// in live mode.
tier_cap_exceeded 422 Your plan's maxWebhookEndpoints would be exceeded.

Retrieve an endpoint

GET /v1/webhook-endpoints/{id}

Returns the webhook endpoint object with signingSecret: null.

plugipay_curl GET '/v1/webhook-endpoints/whep_01HXxxxxxxxxxxxxxxxxxxxxxx'

404 not_found if the ID doesn't exist in this workspace.

List endpoints

GET /v1/webhook-endpoints

Query parameters

Param Type Notes
limit int Default 50, max 100.
cursor string From a previous response's meta.page.nextCursor.
status enum active, disabled, errored. Omit for all.

Returns a paginated array of webhook endpoint objects, newest first.

const { data } = await plugipay.webhookEndpoints.list({ status: 'active' });

Update an endpoint

PATCH /v1/webhook-endpoints/{id}

Request body

Send only the fields you want to change.

Field Type Notes
url string New destination URL.
events string[] Replaces the subscribed event list wholesale — not a diff.
description string | null null clears the description.
enabled boolean false pauses delivery without losing config; true resumes. Use this to re-enable an errored endpoint after fixing the receiver.

Response

200 OK with the updated endpoint. signingSecret is null; to rotate, use the dedicated endpoint below.

plugipay.webhook_endpoints.update(
    "whep_01HXxxxxxxxxxxxxxxxxxxxxxx",
    events=["payment.succeeded", "payment.failed", "refund.succeeded"],
    enabled=True,
)

Changing events is wholesale, not additive. If you currently subscribe to [payment.succeeded] and you PATCH with events: [refund.succeeded], you'll only receive refunds afterward. Read first, merge client-side, then write.

Delete an endpoint

DELETE /v1/webhook-endpoints/{id}

Permanently removes the endpoint and stops all future deliveries. Pending retries are dropped. The signing secret is invalidated immediately.

Returns 204 No Content (with the standard { data: null, error: null, meta: {...} } envelope).

err := client.WebhookEndpoints.Delete(ctx, "whep_01HXxxxxxxxxxxxxxxxxxxxxxx")

If you only want to pause deliveries, update with enabled: false instead — you can re-enable later without re-registering and re-distributing a new secret.

Rotate the signing secret

POST /v1/webhook-endpoints/{id}/rotate-secret

Issues a fresh signingSecret. The previous secret stays valid for 24 hours so you can roll out the new one without dropping deliveries. During the overlap, Plugipay signs each event with both secrets — your verifier accepts a match against either.

Request body

Field Type Notes
overlapSeconds int Optional. How long the old secret remains valid. Default 86400 (24 h). Max 259200 (72 h). Pass 0 to invalidate the old secret immediately.

Response

200 OK with the full webhook endpoint object; signingSecret contains the new value. The old secret is not echoed back.

Node

const rotated = await plugipay.webhookEndpoints.rotateSecret(endpointId);
await secretStore.set('PLUGIPAY_WEBHOOK_SECRET_NEXT', rotated.signingSecret);

The rollout pattern:

  1. Call rotate-secret and store the new value alongside the old one.
  2. Deploy your handler with both secrets; the SDK's verifyWebhook accepts an array.
  3. Once the deploy is healthy, drop the old secret. After the overlap window, Plugipay stops signing with it regardless.

See API → Webhooks → Signing for the verification recipe; the algorithm is the same after rotation.

List recent deliveries

GET /v1/webhook-endpoints/{id}/deliveries

The audit trail of what Plugipay has tried to POST to this endpoint. Useful for debugging silent handlers and reconciling missed events.

Query parameters

Param Type Notes
limit int Default 50, max 100.
cursor string For pagination.
status enum succeeded, failed, pending. Omit for all.
eventType string Filter by event type (e.g., payment.succeeded).
since ISO 8601 | epoch Only deliveries attempted at or after this time. We retain delivery history for 30 days.

Response

{
  "data": [
    {
      "id": "whdel_01HZxxxxxxxxxxxxxxxxxxxxxx",
      "endpointId": "whep_01HXxxxxxxxxxxxxxxxxxxxxxx",
      "eventId": "evt_01HZxxxxxxxxxxxxxxxxxxxxxx",
      "eventType": "payment.succeeded",
      "status": "succeeded",
      "statusCode": 200,
      "durationMs": 142,
      "retryCount": 0,
      "attemptedAt": "2026-05-12T10:55:01.811Z",
      "nextRetryAt": null,
      "responseBodyPreview": "ok"
    }
  ],
  "error": null,
  "meta": { "page": { "limit": 50, "hasMore": true, "nextCursor": "cur_..." } }
}
Field Notes
status succeeded (2xx), failed (4xx, 5xx, timeout, network error), pending (queued for retry).
statusCode HTTP status your server returned. null for connection-level failures.
durationMs Time from connection open to response received. null on timeout.
retryCount 0 for the first attempt, 1+ for retries. The full retry schedule is in API → Webhooks → Retry policy.
nextRetryAt When the next automatic retry is scheduled, if status: pending.
responseBodyPreview First 1 KB of your handler's response body. Truncated; not the full body.
plugipay_curl GET '/v1/webhook-endpoints/whep_01HX.../deliveries?status=failed&limit=20'

Retry a delivery

POST /v1/webhook-endpoints/{id}/deliveries/{deliveryId}/retry

Manually re-queues a single delivery, regardless of where the automatic retry schedule has it. Use after fixing a handler bug to replay missed events, or to drain a backlog faster than the natural cadence after re-enabling an errored endpoint.

Manual retries create a new delivery row (with retryCount incremented) and run in parallel with the automatic schedule, which is not reset.

Response

202 Accepted with the newly queued delivery row in pending status. The actual POST happens within a second or two; poll the deliveries list to see the outcome.

const { data: failed } = await plugipay.webhookEndpoints.listDeliveries(epId, { status: 'failed' });
for (const d of failed) {
  await plugipay.webhookEndpoints.retryDelivery(epId, d.id);
}

Errors

Code Status When
not_found 404 Delivery ID doesn't belong to this endpoint, or is older than 30 days.
already_succeeded 409 The delivery already returned a 2xx; nothing to retry.
endpoint_disabled 422 The endpoint is disabled or errored; re-enable it first.

Events this resource produces

Event When
webhook_endpoint.disabled Plugipay auto-disabled the endpoint after 20 consecutive delivery failures. The endpoint's status is now errored. Re-enable by updating with enabled: true once your receiver is healthy.

The full event catalog (and the events your endpoint will receive) lives in API → Webhooks → Event catalog.

Auto-disable is a circuit breaker. Once a receiver has accumulated 20 failures across the retry curve, continuing to hammer it wastes both sides' resources. The webhook_endpoint.disabled event fires so your monitoring can page someone; the endpoint config and signing secret are preserved for when you flip enabled back to true.

Next

Plugipay — Payments that don't tax your success