Skip to content

Webhooks

Webhooks let Beyond Pricing push events to your backend the moment they happen, so you do not have to poll the API. When something you care about occurs — a reservations refresh finishes, a listing's base price changes — Beyond sends a signed HTTP POST to an endpoint you register.

Every delivery is a JSON:API document signed with Standard Webhooks (HMAC-SHA256), so you can verify it really came from Beyond and was not tampered with in transit.

Availability

Webhooks are available to Partners and to personal users.

  • Partners configure them per OAuth2 application; events fire for the application that owns a resource's user. See Getting Set Up.
  • Personal users configure one endpoint per Beyond account from the dashboard; events fire for everything on that account. See For personal users.

Event Catalog

Each event is documented independently, including exactly when it fires and the full shape of its payload.

Event Event type Resource type Fires when
Account Created account.created account-created-events The background listing sync that follows adding a managed account finishes (success or failure).
Account Refreshed account.refreshed account-refreshed-events The background listing sync started by refreshing a managed account finishes (success or failure).
Listing Created listing.created listing-created-events A new listing owned by one of your users is fully set up (its initial base price is computed).
Listing In Active Market Changed listing.in_active_market_changed listing-in-active-market-changed-events A listing's in-active-market attribute changes — it leaves or re-enters the state where its calendar is guaranteed to work.
Listing Refresh listing.refreshed listing-refreshed-events A partner-triggered listing refresh chain finishes (success or failure).
Listing Base Price Changed listing.base_price_changed listing-base-price-changed-events A listing's effective base price or currency changes on any Beyond surface; enabled marks live vs. speculative changes.
Webhook Ping webhook.ping webhook-ping-events A personal user saves a webhook URL or sends a test ping from the dashboard. Verification only; acknowledge and ignore.

Getting Set Up

Webhook delivery is configured per OAuth2 application. To start receiving events, provide Beyond with:

  1. An endpoint URL — an HTTPS URL on your backend that accepts POST requests.
  2. Beyond returns a signing secret in the form whsec_<base64-key>. Store it securely; you need it to verify every delivery.

Contact Beyond during onboarding to register your endpoint and receive your secret, the same way IP allowlists are provisioned for partner applications. If an application has no endpoint or secret configured, matching events are simply not delivered — nothing else changes.

Once you are set up you can manage the endpoint yourself, without contacting us: Webhook Configuration reads and updates your destination URL and your event selection, behind the webhooks:write scope.

For Personal Users

If you automate your own Beyond account with a personal access token, you configure webhooks yourself from the dashboard's Personal Access Tokens page. Only an administrator of the account can manage them, and the same Beyond Pro requirement as for personal access tokens applies.

  1. Generate a signing secret. Beyond shows the whsec_… value once. Add it to your receiver first: every delivery, including the verification ping, is signed with it.
  2. Save your endpoint URL. It must be a public https URL. Beyond sends a signed webhook.ping to it and saves the URL only when your endpoint answers with a 2xx. Redirects are not followed, and a failed ping leaves your previous settings untouched; the dashboard shows why it failed.
  3. Choose your events. You receive every event type by default, including ones added later. Untick "All events" to pin an explicit list.

After that you can send a test ping at any time, rotate the secret (the new value is shown once and applies to the next delivery; update your receiver first), or remove the endpoint, which stops delivery immediately.

Events about your account fire the same way as for partners: a new listing, a base price change, and so on. You can read back what Beyond delivered with a PAT through the Webhook Events endpoints; personal access tokens carry the webhooks:read scope.

Choosing Your Events

By default an application receives every event type, including ones Beyond adds in future — that is what subscribed-events: ["*"] means, and it is what you get unless you change it. Nothing in the catalog above is opt-in.

If your receiver only handles some of these, narrow the list to exactly the types you process:

PATCH /api/v1/webhook-configuration/
{
  "data": {
    "type": "webhook-configurations",
    "id": "your-client-id",
    "attributes": {
      "subscribed-events": ["listing.created", "listing.base_price_changed"]
    }
  }
}

The trade-off is deliberate: pinning an explicit list means a new event type Beyond ships is not delivered until you add it, so you are never surprised by a payload you did not plan for — but you also have to come back for anything new. See Webhook Configuration.

Unsubscribing applies to every event, including those that report on a job you started yourself: turn off listing.refreshed and a refresh you requested through the API completes without notifying you.

Changing Your Destination URL

The same endpoint moves your deliveries to a new URL. For partners there is no verification step — the new URL is used from the moment the write succeeds, so confirm it is live and verifying signatures first. (Personal users saving a URL from the dashboard get a verification ping instead; see For personal users.) Deliveries already in flight, including retries of earlier events, may still land on the old URL for a short period; keep both endpoints up across a cutover if you cannot tolerate a gap.

Setting webhook-url to "" stops delivery while keeping your event selection intact, which is the clean way to pause webhooks.

Rotation

Treat the signing secret like a credential. If it is ever exposed, ask Beyond to rotate it. Rotation is currently a hard cutover: deliveries are signed with the old secret until the change lands, and with the new one immediately after, with no overlap window. Coordinate a rotation window with us so you can swap the secret on your side at the same moment. (The webhook-signature header is a list precisely so that an overlap period can be added later without changing your verification code.)

Delivery Model

sequenceDiagram
    participant BP as Beyond Pricing
    participant N as Notifications Service
    participant P as Your Endpoint

    BP->>N: Event occurs, enqueue delivery
    N->>N: Build deterministic body, sign (HMAC-SHA256)
    N->>P: POST signed JSON:API document
    alt Any 2xx
        P-->>N: 200 OK
        Note over N: Delivered — done
    else 5xx / 408 / 429 / timeout / network error
        P-->>N: error
        N->>P: Retry with exponential backoff (up to 5 attempts)
    else Other 4xx (incl. 410)
        P-->>N: error
        Note over N: Not retried
    end
  • Transport: HTTPS POST to your registered URL.
  • Content-Type: application/vnd.api+json (a JSON:API document).
  • Body: byte-stable JSON with deterministic key ordering. The signature is computed over the raw bytes — verify and parse those exact bytes, do not re-serialize before verifying.

Request Headers

Every delivery carries the Standard Webhooks headers:

Header Description
webhook-id Stable event identifier (msg_<base32 ULID>). Re-used across delivery retries — use it as your idempotency key. Matches data.id in the body.
webhook-timestamp Unix time (seconds) when this attempt was sent. Re-stamped on every retry. Reject deliveries whose timestamp is more than ~5 minutes from your clock to defend against replay.
webhook-signature Space-separated list of v<version>,<base64-signature> entries (currently just v1,…). See verification.

Verifying Signatures

Always verify the signature before trusting or parsing the body.

The signed content is the string {webhook-id}.{webhook-timestamp}.{raw-body}, and the signature is base64(HMAC-SHA256(key, signed_content)), where the key is your secret with the whsec_ prefix stripped and base64-decoded to 32 raw bytes.

Use an official library

The simplest, safest path is a Standard Webhooks library for your language — it handles the timestamp tolerance and constant-time comparison for you. Pass it your whsec_… secret, the request headers, and the raw body.

If you verify manually, mirror this recipe (Python):

import base64
import hashlib
import hmac
import time


def verify(secret: str, headers: dict[str, str], raw_body: bytes) -> bool:
    """Return True if the delivery is authentic and fresh."""
    msg_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]

    # Replay protection: reject deliveries that are too old (or from the future).
    if abs(time.time() - int(timestamp)) > 300:
        return False

    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed_content = f"{msg_id}.{timestamp}.".encode("utf-8") + raw_body
    expected = base64.b64encode(
        hmac.new(key, signed_content, hashlib.sha256).digest()
    ).decode("ascii")

    # webhook-signature is a space-separated list of "v<version>,<signature>".
    for entry in headers["webhook-signature"].split(" "):
        version, _, signature = entry.partition(",")
        if version == "v1" and hmac.compare_digest(signature, expected):
            return True
    return False

Always compare in constant time

Use a constant-time comparison (hmac.compare_digest above) so a naive string comparison cannot leak the signature byte-by-byte through timing.

Responding to Deliveries

  • Acknowledge fast. Return any 2xx as soon as you have safely received the event (for example, after persisting it to a queue). Do the real work asynchronously — slow handlers risk a timeout, which Beyond treats as a failure and retries.
  • Return 410 Gone if the endpoint is permanently retired. Beyond stops retrying that delivery; future iterations may auto-disable endpoints that respond 410.

Retries

Deliveries that do not succeed are retried automatically:

Response Behavior
Any 2xx Delivered. No retry.
5xx, 408, 429, timeout, or network error Retried with exponential backoff.
Other 4xx (including 410) Not retried — treated as a permanent rejection.

Beyond makes up to 5 attempts (the initial delivery plus 4 retries) with exponential backoff (roughly 1s, 5s, 25s, 125s, capped at 300s), honoring a Retry-After header when you send one. After the final attempt, the delivery is dropped.

If your receiver was down and you need to recover events you missed, you can read back what Beyond delivered — including the delivery status and each event's body — via the Webhook Events endpoints (webhooks:read scope).

Idempotency

Because retries re-use the same webhook-id, the same event may arrive more than once. Make your handler idempotent: dedupe on webhook-id (equivalently, data.id in the body) and treat a repeat as a no-op. A given webhook-id always represents the same event with the same body.

Payload Envelope

Every event shares the same JSON:API envelope. The meta block is identical across events; data is event-specific and documented on each event's page.

{
  "meta": {
    "type": "listing.refreshed",
    "sent-at": "2026-07-03T12:34:56Z"
  },
  "data": {
    "type": "listing-refreshed-events",
    "id": "msg_01J9Z6M0K3QK7YV8N2C4B5A6D7",
    "attributes": { "...": "event-specific" },
    "relationships": { "...": "event-specific" }
  }
}
meta field Description
type The event type (e.g. listing.refreshed). Your routing key, and where the version lives if one is ever introduced — see Versioning.
sent-at RFC 3339 UTC timestamp when the envelope was assembled. Not when the thing happened.

The event's identifier appears exactly twice: in the webhook-id header, and as data.id. There is no schema-version field: if an event ever needs a breaking change, it ships under a new event type rather than a bumped envelope — see Versioning.

Two kinds of type

An event carries two type fields and they do different jobs:

  • meta.type (listing.refreshed) is the event type — route on this, and acknowledge (2xx) any type you do not recognize. New event types (including a .v2 of one you handle) are delivered as soon as they exist — see Versioning.
  • data.type (listing-refreshed-events) is the JSON:API resource type — deserialize on this.

See the JSON:API guide for why the event type lives under meta.

data.id is the event id, not a domain id

The document you receive describes the event, so data.id is the event's ULID — the same value as the webhook-id header, and what you dedupe on. It is not the id of the listing or account the event is about. The domain object is always reached through relationships — its id is in relationships.<name>.data.id, and relationships.<name>.links.related is a URL you can GET for its current state.

sent-at is not the occurrence time

meta.sent-at stamps the envelope. When the thing actually happened lives in the event's own attributes — completed-at, changed-at, or created-at, depending on the event. They can differ by minutes, and on a retry sent-at stays fixed while the webhook-timestamp header is re-stamped.

Field names are dasherized

Like the rest of the API, event field names use dashes (base-price, completed-at, channel-listing-id). See the JSON:API guide.

OpenAPI / Machine-Readable Schema

The webhook payloads are published in the OpenAPI 3.1 schema as top-level webhooks, alongside the request/response endpoints. Use them to generate types or validate payloads.

Tool URL Notes
OpenAPI Schema /api/v1/schema/ Downloadable OpenAPI 3.1 spec; webhook bodies live under the top-level webhooks key, one per event.
ReDoc /api/v1/redoc/ Renders the webhooks section under its own heading.

Each event contributes one webhook (named in camelCase — listing.refreshedlistingRefreshed) tagged Webhooks, documenting its body schema and the Standard Webhooks headers.

Next Steps