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. |
signingSecretis shown only on create and rotate-secret. Every other endpoint returns it asnull. 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
eventsis wholesale, not additive. If you currently subscribe to[payment.succeeded]and youPATCHwithevents: [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:
- Call rotate-secret and store the new value alongside the old one.
- Deploy your handler with both secrets; the SDK's
verifyWebhookaccepts an array. - 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.disabledevent fires so your monitoring can page someone; the endpoint config and signing secret are preserved for when you flipenabledback totrue.
Next
- API → Webhooks — event payloads, the full event catalog, signature verification, retry policy.
- Portal → Webhooks — the same surface, through the dashboard.
- Resources → Events — query the underlying event history that drives deliveries.
- CLI → Webhooks —
plugipay webhooks listenfor local development.