Webhooks

Manage your own webhook endpoints through the API, receive every connected partner's events on one endpoint, and route each event by its partnerId.

Localoy tells your server when something happens for a connected partner — one of your items changes, a customer books, a payment settles, the partner changes your access — with a signed HTTPS POST. The envelope, signatures and retries are the ones partners get. Three things differ:

  1. You manage your endpoints through the API, with your provider key. There is no portal for providers.
  2. One endpoint serves every partner. Each event carries the partnerId it belongs to.
  3. What you receive follows each connection — the scopes it grants, your modules and its status.

One endpoint for every partner#

Routing an event to the right partner
Routing an event to the right partnerLocaloyYour endpointYour job queue1. POST event + X-Localoy-Signature2. Verify signature, skip repeats3. Enqueue with partnerId4. 200 OK within 10 s5. Find the partner's account6. Apply the change
  1. Localoy → Your endpoint: POST event + X-Localoy-Signature
  2. Your endpoint → Your endpoint: Verify signature, skip repeats
  3. Your endpoint → Your job queue: Enqueue with partnerId
  4. Your endpoint → Localoy: 200 OK within 10 s
  5. Your job queue → Your job queue: Find the partner's account
  6. Your job queue → Your job queue: Apply the change
Envelope
{
  "id": "evt_mfz3k2a1Xq9vT0bLm3Rk",
  "event": "booking.created",
  "occurredAt": "2026-09-27T11:05:42.230Z",
  "environment": "production",
  "partnerId": "cm1partnerid000000000000001",
  "data": { "booking": { "…": "…" } }
}

Find the partner by partnerId — the ID you send as X-Localoy-Partner-Id. For a partner you no longer know, answer 2xx anyway, so the event is not retried.

Set up an endpoint#

cURL
curl -X POST "$LOCALOY_BASE_URL/webhooks" \
  -H "Authorization: Bearer $LOCALOY_PROVIDER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://hooks.example.com/localoy",
    "environment": "PRODUCTION",
    "events": ["booking.created", "booking.updated", "booking.cancelled", "connection.updated", "connection.revoked"],
    "description": "Production receiver"
  }'
FieldDetail
urlWhere Localoy sends events: https, public, and within the URL requirements that apply to every Localoy webhook.
environmentSANDBOX or PRODUCTION. An endpoint receives only its environment's events, and its environment cannot be changed later.
eventsThe events to send it. At least one.
descriptionOptional, up to 200 characters.

The response carries the endpoint's signing secret, starting whsec_. Store it in your receiver. You can reveal it again and rotate it later.

You can hold up to 10 endpoints, and the same URL cannot be registered twice in one environment. Every endpoint operation is in the reference.

Events#

EventSent whenYou receive it ifEnvironment
catalog.item.created, catalog.item.updated, catalog.item.deletedAn item is created, changed or deleted.It is an item you created for that partner.That of the key that made the change.
booking.created, booking.updated, booking.cancelledA customer books with the partner, or a booking changes or is cancelled.The partner granted BOOKINGS, and the booking is in one of your modules.Production.
payment.updatedA booking's payment status changes.The partner granted BOOKINGS and PAYMENT, and the booking is in one of your modules.Production.
connection.updatedThe partner pauses you, resumes you or changes your scopes.Always.See Environments.
connection.revokedThe partner disconnects you.Always.See Environments.
pingYou call POST /webhooks/{id}/test. It cannot be subscribed to.—The endpoint's.

Booking and payment events carry the same booking object partners receive, including customerName, customerPhone and customerEmail — see Booking events. Catalogue events carry the item, as in Catalogue events. The two connection.* payloads are in Connection events; only Technology Providers can subscribe to them.

Environments#

  • booking.* and payment.updated go only to production endpoints: real bookings exist only in production.
  • catalog.item.* go to your endpoints of the environment of the key that made the change.
  • connection.* come from the partner's own actions in its Partner Portal, not from one of your keys, so they go to every endpoint subscribed to them in both environments, under one event ID. Each delivery's environment is its endpoint's.
  • A test ping goes to the endpoint you tested, with partnerId null.

When nothing is delivered#

SituationWhat you receive
The partner paused you.connection.updated with reason paused, then no new events for that partner until connection.updated with reason resumed. Deliveries queued before the pause are still sent. Meanwhile, replaying one of its deliveries answers 409 webhook_replay_connection_inactive.
The partner disconnected you.connection.revoked, then nothing for that partner — not even a catalog.item.deleted for the items removed with the connection; itemsRemoved counts them. Deliveries still queued for it are marked FAILED without being sent, and replaying one of its earlier deliveries answers 409 webhook_replay_connection_inactive. A connection.* delivery can still be replayed.
Localoy suspended you.Nothing, to any of your endpoints, until Localoy resumes you. Deliveries already queued are marked FAILED without being sent; replay them once you are resumed.
Your endpoint is disabled.Nothing to that endpoint. An endpoint is disabled automatically after 10 consecutive failed attempts.

Verifying signatures#

Deliveries are signed exactly like partners' webhooks, each with its endpoint's own secret:

Header
X-Localoy-Signature: t=1790500000,v1=5b1f0c9e4a…

v1 is the hex HMAC-SHA256 of {t}.{raw body}, keyed with the endpoint's full whsec_… secret. Follow Verifying signatures: its Node.js, PHP and Python code works unchanged. The other headers — X-Localoy-Event-Id for de-duplication among them — are listed in Receive an event.

Delivery and retries#

The policy is the one in Delivery and retries: answer 2xx within 10 seconds; a failed delivery is retried, up to 6 attempts over about five minutes; and an endpoint is disabled after 10 consecutive failed attempts. Delivery is at least once, and in no guaranteed order.

Your delivery log#

TaskEndpoint
List deliveries, by partner, event, status or endpointGET /webhooks/deliveries
Read one, with its payload and your endpoint's last answerGET /webhooks/deliveries/{id}
Send it againPOST /webhooks/deliveries/{id}/replay
Switch a disabled endpoint back onPATCH /webhooks/{id} with status ENABLED

To recover after your endpoint was down:

Recovering after an outage
Recovering after an outageYour serverLocaloyYour endpoint1. POST /webhooks/{id}/test2. Signed ping3. 2004. 200 ok: true5. PATCH /webhooks/{id} ENABLED6. GET /webhooks/deliveries?status=FAILED7. The failed deliveries8. POST …/{id}/replay, for each9. Same event ID, new delivery
  1. Your server → Localoy: POST /webhooks/{id}/test
  2. Localoy → Your endpoint: Signed ping
  3. Your endpoint → Localoy: 200
  4. Localoy → Your server: 200 ok: true
  5. Your server → Localoy: PATCH /webhooks/{id} ENABLED
  6. Your server → Localoy: GET /webhooks/deliveries?status=FAILED
  7. Localoy → Your server: The failed deliveries
  8. Your server → Localoy: POST …/{id}/replay, for each
  9. Localoy → Your endpoint: Same event ID, new delivery

A test works on a disabled endpoint. Enabling an endpoint resets its failure count. A replay keeps the event's ID, so a receiver that already processed the event recognises it.