<!-- https://unlockos.io/manual/webhook-channel -->

# Webhook Notification Channel Help

## Overview

**Webhook** is the **6th channel** in [Notification Workflows](notification-workflow.md). Like LINE / Email / SMS / Voice / Speaker, it uses the same "anchor event + offset" mechanism, but instead of notifying a person it delivers reservation events to **any external endpoint** via an HTTPS POST. Use it to feed reservation events into your own systems or third-party services (inventory sync, analytics, custom alerting, etc.).

Open it from the sidebar via "🔔 Notification Workflows" → the **Channels** tab → the Webhook card, which links to `/notifications/channels/webhook` (endpoint management).

> **This feature is enabled per organization.** If the Webhook card doesn't appear on the Channels tab, the `notification_webhook_channel` feature flag is OFF for your organization. Ask support to enable it.

Registering an endpoint here is only half the setup. To actually make Webhook fire, open a workflow in the **Workflows tab** of [Notification Workflows](notification-workflow.md), set its channel to "Webhook," and pick the endpoint to send to. This page manages endpoints and signing keys; the Workflows tab decides which event triggers a send.

---

## Detailed features

## Feature 1: Registering endpoints

1. Enter the receiving URL in the URL field (**https only**)
2. Optionally enter a description to help identify the endpoint
3. Click "Add"

On success, the **signing secret is shown once**. It is not shown again after you leave the page, so copy it into your receiving system right away (you can retrieve it again later via "Reveal secret" — see [Feature 2](#feature-2-signing-secret-and-rotation)).

Each registered endpoint appears as a card with the following actions.

| Action | What it does |
|------|------|
| Edit | Change the URL or description |
| Enabled/disabled toggle | Disabling stops deliveries to this endpoint (the record itself is kept) |
| Reveal secret | Show the current signing secret again |
| Rotate secret | Issue a new signing secret (see [Feature 2](#feature-2-signing-secret-and-rotation)) |

> **Endpoints cannot be deleted.** Switch an endpoint you no longer use to "disabled" instead — this keeps past delivery history and workflow configuration intact.

### URL restrictions (what won't be accepted)

The URL is validated both when you save it and again right before each send. The following are rejected:

- Anything other than `https:` (`http:` is not accepted)
- Hostnames that resolve to private / loopback / link-local or similar internal addresses
- Numeric-IP and similar notations that try to work around the above

Even a URL that was accepted at registration time will be rejected on the next send if its DNS is later repointed to an internal address.

### Organization-shared endpoints

If a sibling facility in the same organization has registered an endpoint, it appears in your list with an "Org-shared" badge. This is read-only — **you cannot edit, disable, or manage the secret for it from your facility's screen** (the URL itself is hidden; only an identifying hostname is shown). Events are still delivered to org-shared endpoints, so they do appear as a selectable destination when configuring a workflow.

---

## Feature 2: Signing secret and rotation

Every request carries an HMAC signature so the receiver can verify it came from UnlockOS.

### What's in the signature

| Header | Content |
|---|---|
| `X-UnlockOS-Signature` | `sha256=` followed by the signature (base64). During rotation, two may appear space-separated |
| `X-UnlockOS-Timestamp` | The send time (Unix seconds) included in what was signed |
| `X-UnlockOS-Endpoint-Id` | Identifies which endpoint (and therefore which secret) to verify against |
| `X-UnlockOS-Delivery-Id` | The idempotency key for this delivery (same as the body's `eventId`) |
| `X-UnlockOS-Delivery-Attempt` | Which attempt this is (starts at 1, increases on retry) |
| `X-UnlockOS-Event-Type` | Same value as the body's `eventType` |

The signature is `base64(HMAC-SHA256(signing_secret, "timestamp" + "." + "raw request body bytes"))`. To verify it on your side:

1. Use `X-UnlockOS-Endpoint-Id` to look up the matching secret in your own records (don't use the organization/facility IDs inside the body to choose the key — anything read before verification can't be trusted)
2. Recompute the same value using that secret, the received `X-UnlockOS-Timestamp`, and the **raw received body bytes** (not a JSON value re-serialized after parsing)
3. Compare it against `X-UnlockOS-Signature` — there may be more than one `sha256=` value; **accept if any one of them matches** (assuming there is only ever one will break delivery during key rotation, described below)
4. Confirm `X-UnlockOS-Timestamp` isn't far from the current time (roughly within 5 minutes) to guard against replay
5. Even after signature and timestamp checks pass, discard the request if you've already processed this `eventId` before (delivery is at-least-once, so the same event can arrive more than once)

### Rotating the secret

Clicking "Rotate secret" issues a new secret while keeping the previous one valid **for one generation**, so verification keeps working while you update the receiving side — no downtime. You'll be asked to confirm before it runs.

"Reveal secret" shows the current secret again on demand, but repeating it too quickly within a short window is temporarily rate-limited as an anti-abuse measure. Wait a bit and try again.

---

## Feature 3: Payload shape

The request body (envelope) sent looks like this. **Organization, facility, and environment fields are assembled by the system automatically and cannot be edited by the facility owner** — letting tenant identifiers be edited would risk a receiver mistaking one tenant's event for another's due to typos or omissions.

```jsonc
{
  "schemaVersion": "1",
  "eventType": "reservation.created",
  "eventId": "idempotency key for this delivery, unchanged across retries",
  "sentAt": "send time (ISO 8601)",
  "environment": "production | staging | development",

  "endpoint": { "id": "the endpoint's id" },
  "organization": { "id": "...", "name": "..." },
  "facility": { "id": "...", "name": "...", "slug": "...", "timezone": "Asia/Tokyo" },
  "workflow": { "id": "...", "key": "...", "anchorEvent": "..." },
  "source": { "type": "reservation" },

  "data": { },        // shape varies per eventType
  "customData": { }   // free-form field the facility owner can add (not yet exposed in the UI — planned)
}
```

- **Guest name, email, and phone number are never included.** Personal information is minimized by design
- `environment` lets you tell staging and production apart even if they post to the same URL
- `facility.slug` can change, so route on `facility.id` / `organization.id` instead

### v1 event list

| `eventType` | Fires when |
|---|---|
| `reservation.created` | A reservation is created |
| `reservation.cancelled` | A reservation is cancelled |
| `reservation.extended` | A reservation is extended |
| `reservation.approval_pending` | A reservation enters the pending-approval state |
| `reservation.started` | The reservation's start time is reached |
| `reservation.ended` | The reservation's end time is reached |
| `checkin.completed` | Check-in is completed |
| `checkout.completed` | Check-out is completed |
| `payment.completed` | Payment is completed |

Which event triggers a Webhook send is set by choosing the anchor event on the workflow's edit screen in [Notification Workflows](notification-workflow.md).

---

## Feature 4: Retries and failure behavior

| Response | Behavior |
|---|---|
| 2xx | Success. Recorded as "sent" on the History tab |
| 429 / 408 | Treated as transient and retried |
| Other 4xx | **Permanent failure. Not retried** (treated as a misconfigured destination that won't change on resend) |
| 3xx (redirect) | **Permanent failure. Redirects are never followed** (to avoid opening a bypass path) |
| 5xx / timeout / connection error | Treated as transient and retried (up to 5 attempts, then marked "failed") |

- The response timeout is 5 seconds
- `eventId` and the signed body never change across retries — only the attempt count (`X-UnlockOS-Delivery-Attempt`) increases
- Delivery results can be checked on the **History tab** of [Notification Workflows](notification-workflow.md), filtered to channel "Webhook" (the same screen used for every other channel)

---

## Feature 5: Billing

The Webhook channel is **free**. Since it's a plain HTTP request with no third-party cost, it isn't metered the way SMS, Voice, and Speaker are.

---

## Feature 6: Choosing between channels

| Channel | Recipient | Eligible for "Auto"? | Billing |
|---|---|---|---|
| Email / LINE / SMS | A guest or an individual admin | Yes (routed automatically to whichever is reachable) | Email/LINE free, SMS metered |
| Voice / Speaker | A phone call / an in-space speaker | No (explicit selection only) | Metered |
| **Webhook** | **An external system's endpoint (not a person)** | **No (explicit selection only)** | **Free** |

Webhook isn't a channel for notifying a person — it's a channel for feeding events into a system. That's why it's never chosen by "Auto"; you must explicitly select "Webhook" on the workflow's edit screen and pick a destination endpoint.

---

## FAQ

### Q: Can I see the URL of an org-shared endpoint?
A: No. Your facility's screen only shows an identifying hostname — the full URL (which, without a signature check, is itself sensitive) is hidden.

### Q: I want to delete an endpoint
A: Deletion isn't supported. Switch it to "disabled" instead — this preserves past delivery history and workflow configuration.

### Q: Which events trigger Webhook?
A: Whatever anchor event is set on a workflow whose channel is "Webhook" in [Notification Workflows](notification-workflow.md). This works for both standard and custom workflows.

### Q: Where do I set `customData`?
A: There's no input field for it in the current admin UI yet (planned for a future update). Right now only the system-assembled standard fields are included in the body.

### Q: I lost my signing secret
A: Use "Reveal secret" to show it again. This requires owner-level access, and repeating it in quick succession is rate-limited.

### Q: Can I register a URL that redirects?
A: You can register it, but redirects are never followed at send time — a 3xx response is treated as a permanent failure. Register the final, non-redirecting URL instead.

---

## Related pages

- [Notification Workflow](notification-workflow.md) - configure the anchor event and channel selection here
- [Speaker Notification Channel](space-speaker.md)
