Webhooks
Publica.la sends a signed JSON POST request to each of your endpoints when an event it subscribes to happens in your store.
They replace the deprecated legacy sale webhook. Both can run at the same time; its page lists what changes.
Events
| Event | Sent when |
|---|---|
order.paid | The first approved sale payment of an order is recorded. |
charge.paid | A payment gateway charge is approved, renewals included. |
Endpoints
Store administrators manage endpoints in the dashboard, in Settings > Webhooks (/dashboard/settings#webhooks). Plan administrators cannot.
| Property | Description |
|---|---|
| URL | Where events are sent. See the rules below. |
| Name | Optional label. |
| Events | The event types the endpoint receives. |
| Enabled | When cleared, the endpoint is paused. |
| Signing key | 64 hexadecimal characters. Shown once, on creation and rotation. |
A store can have up to 10 endpoints.
The URL must:
- Use
httporhttps. Withhttp, events travel unencrypted. - Use port 80 or 443 if it sets a port.
- Resolve only to public IP addresses. This is checked again on every delivery.
Redirects are not followed. Requests come from the addresses in Platform IP Addresses.
| Action | Effect |
|---|---|
| Edit | Changing the URL resets the failure count and re-enables a disabled endpoint. |
| Pause | Nothing is delivered. Events that happen while paused are dropped, not queued. |
| Rotate signing key | The previous key stops working immediately. |
| Send test | Sends a test delivery. Up to 5 per endpoint per minute. |
| Re-enable | Shown on an endpoint Publica.la disabled, with the last error. Also resumes a paused one. |
| Delete | Immediate and permanent. |
Recent deliveries lists the last 50 delivery attempts, kept for 30 days, with status Delivered (a 2xx), Failed (retried while attempts remain) or Skipped (not sent: the endpoint was paused, disabled or deleted, or the event had already reached it).
Request
Headers
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | publica.la webhooks (+https://publica.la) |
X-Pla-Signature | t=<unix timestamp>,v1=<hex HMAC-SHA256>. See Signature. |
X-Pla-Event | Event type, for example order.paid. Equal to type in the body. |
X-Pla-Delivery | Delivery id, evt_ followed by a UUID. Equal to id in the body. |
X-Pla-Idempotency-Key | Key of the fact the event announces, for example order.paid:order:1001. Equal to idempotency_key. |
Envelope
{
"id": "evt_7c1d4e92-3b6a-4f0e-8a15-d2e9b0c47f36",
"type": "order.paid",
"created_at": "2026-01-15T14:30:06Z",
"idempotency_key": "order.paid:order:1001",
"tenant": {
"id": "42",
"slug": "yourstore"
},
"data": {}
}
| Field | Type | Description |
|---|---|---|
id | string | Delivery id. Unique per endpoint: two endpoints get different ids for one event. |
type | string | Event type. |
created_at | string | ISO 8601 UTC time the delivery was queued. |
idempotency_key | string | Stable key of the fact the event announces. Same for every endpoint and every retry. |
tenant | object | Your store: id (string) and slug (string). |
data | object | Event payload, described on each event's page. |
The body, id and created_at do not change across retries.
Payload conventions
- Ids are strings.
- Money amounts are integers in the currency's minor unit, in fields ending in
_in_cents:1349is 13.49 USD,11900is 11,900 CLP. - Dates are ISO 8601 strings in UTC.
- Every key is always present. A missing value is
null. - The payload is not versioned. New keys and event types can be added at any time. Renaming, removing or retyping a key ships as a new event type.
Signature
X-Pla-Signature is t=<timestamp>,v1=<signature>:
t: Unix timestamp in seconds when the request was signed. Each retry is signed again with a newt.v1: lowercase hex HMAC-SHA256 of the string<t>.<raw body>, keyed with the signing key as is (not hex-decoded).
To verify a request:
- Split the header on
,, and each part on the first=. - Reject it if
tis more than 300 seconds away from your current time, in either direction. - Compute the HMAC over the raw body bytes as received, not over re-encoded JSON.
- Compare it with
v1in constant time.
Test vector:
| Input | Value |
|---|---|
| Signing key | 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef |
| Body | {"id":"evt_00000000-0000-4000-8000-000000000000","type":"order.paid"} |
| Header | t=1767225600,v1=cfdb027237491eed8330ae6e8aa4a543fa01895ab694b18d1095f4fb22cf23bb |
Delivery
| Property | Value |
|---|---|
| Success | Any 2xx status. The response body is ignored. |
| Failure | Any other status, 3xx included; no response within 10 seconds; a connection error; a URL that no longer resolves only to public IP addresses. |
| Attempts | Up to 12 per delivery, over about 70 hours. |
| Order | Not guaranteed, across events or endpoints. |
| Duplicates | Possible: delivery is at least once. |
Retries
Wait after each failed attempt:
| After attempt | Wait |
|---|---|
| 1 | 1 minute |
| 2 | 5 minutes |
| 3 | 15 minutes |
| 4 | 1 hour |
| 5 | 3 hours |
| 6 | 6 hours |
| 7 to 11 | 12 hours |
A delivery whose 12 attempts fail is exhausted and not sent again.
Duplicates
The same event can arrive more than once, for example when your endpoint processed a request but its response took longer than 10 seconds. Deduplicate on X-Pla-Idempotency-Key, not on X-Pla-Delivery, which differs per endpoint.
Automatic disabling
Publica.la disables an endpoint when both are true:
- Its last 5 deliveries were exhausted.
- It has had no successful delivery in the last 72 hours, or since it was created if that was less than 72 hours ago.
A successful delivery resets the count. A disabled endpoint receives nothing: new events are dropped and pending retries are cancelled. No notice is sent; the dashboard shows it as Disabled until it is re-enabled.
Test deliveries
Send test sends the example payload of the first event in the Events table the endpoint receives (or of the first event in the table if it receives none), with real headers and signature. Its ids are fictitious. Unlike a real delivery:
- Its idempotency key is
test:followed by a UUID. - It is attempted once.
- It is sent even when the endpoint is paused or disabled.
- It does not count towards automatic disabling.
Related Documentation
order.paid: The order paid event.charge.paid: The charge paid event.- Legacy sale webhook: The deprecated JWT sale webhook, and how to migrate from it.