public

Create a webhook

post/public/v1/directories/{directory}/webhooks

Request

  • Base URL: https://api.epicdirectories.com
  • URL: https://api.epicdirectories.com/public/v1/directories/{directory}/webhooks
  • Auth: API key in header x-api-key

Path parameters

directorystring required

Headers

Idempotency-Keystring

Optional. Send a unique key per logical operation to make retries safe. 1-256 characters, and only letters, digits, - and _ — colons and other separators are rejected with 400. A retry with the same key replays the original response (status + body) instead of re-executing. Cached for 24h. Reusing a key with a different request body returns 409 — note that a timestamp in the body (e.g. fetchedAt) turns an otherwise-stable key into a permanent 409, so either omit it (snapshots default it server-side) or fold a payload digest into the key. Skipped on 5xx responses (transient errors should be retried fresh).

x-api-keystring required

API Key for authentication

Request body

namestring

Friendly name for the webhook (e.g. "CF Pages deploy hook")

urlstring required

Target URL — POSTed on subscribed events

eventsstring[]

Subscribed event names. Empty array = all events fire this webhook. Known events: listing.created, listing.updated, listing.deleted — fired on writes via either admin or public API; listing.published — a listing scheduled with publishAt reached its time and is now published. Subscribe to this to stagger a bulk release: schedule the listings apart from one another and each tranche notifies you as it lands, instead of your sitemap gaining every URL at once. Payload is { listingId, listingType, slug }, or the bulk shape { bulk: true, count, listingIds, truncated } when several cross inside one debounce window. NOT fired for a listing whose publishAt passed long before the scheduler saw it — scheduled publishing is evaluated at read time, so such a listing was already public and listing.published would be describing the past; submission.received — an anonymous suggestion or feedback item needs review. Fired ONLY for items that reach pending; spam (honeypot or failed Turnstile) is stored silently and never notifies, so the channel stays worth reading. Payload: { submissionId, kind, listingId }; review.received — a public review is waiting for moderation. Like submission.received, fired ONLY for items that reach pending. Payload: { reviewId, listingId, rating }; review.approved — a review was published and the listing's rating average moved. Consumers that render ratings from a build-time export need this one to know to rebuild. Payload: { reviewId, listingId, rating }; review.rejected — a review that WAS published is no longer public, because it was unpublished, rejected or filed as spam. The mirror of review.approved, and the more urgent of the pair: it is what a consumer rebuilds on to take defamatory or abusive text off their site. Fired ONLY on the approved -> not-approved transition — rejecting a pending review was never public and fires nothing. Payload: { reviewId, listingId, rating }; comment.received — a page comment is waiting for moderation. Fired ONLY for items that reach pending. Payload: { commentId, path }; comment.approved — a page comment was published. A static site that renders comments rebuilds on this. Payload: { commentId, path }; comment.rejected — a page comment that WAS published is no longer public (rejected, unpublished or filed as spam). Fired ONLY on the approved -> not-approved transition; subscribe to it if you render comments, since it is what takes one down. Payload: { commentId, path }; comment.updated — the editors' reply on a currently-approved comment was set, changed or cleared. Not fired for replies on unpublished comments. Payload: { commentId, path }; listing.boosted — a listing became a premium listing, by purchase or by an operator grant. Payload: { listingId, paymentId?, amountCents?, currency? }. A listing.updated is ALWAYS fired alongside it, so a consumer already subscribed to that will rebuild without subscribing to this one; subscribe to this if you want to treat a boost specially. test.ping — only fired from the test endpoint, bypasses debounce. Unknown names are REJECTED with 400. They used to be accepted for forward-compat, but a typo then produced a webhook that looked healthy and never fired — subscribe to a new event once it exists.

isActiveboolean

Disable without deleting

secretstring

Shared secret for HMAC-SHA256 payload signing. When set, every dispatch includes an X-ED-Signature: sha256=<hex> header — verify on your end by computing HMAC-SHA256 of the raw request body using this secret. Verify each ATTEMPT on its own bytes: a retry re-serialises the body with a fresh timestamp, so the signature legitimately differs between attempts of the same delivery. To deduplicate, use X-ED-Delivery-Id, which is stable across all attempts — see §13 of the integrator changelog. If unset, no signature header is sent (delivery still works). Write-only: no endpoint ever returns it, not even the create response. Responses carry hasSecret: true|false instead. Store your copy at the point you generate it — if you lose it, PATCH a new one rather than expecting to read the old one back.

debounceMsnumber

Debounce window in milliseconds. Multiple events within the window collapse to a single dispatch — useful for collapsing bulk imports into one rebuild. NULL inherits the global default (30 000ms / 30s). Range: 0 (no debounce) to 600 000 (10 minutes) — the full range is honoured, including values ABOVE the global default, which used to be silently capped. When multiple webhooks for a directory specify different values, the smallest wins for the directory as a whole; webhooks leaving it NULL express no preference and do not drag the others down — v1 limitation.

Example request

{
  "events": [
    "listing.created",
    "listing.updated"
  ]
}

Response

Webhook created

Changes

No recorded changes to this operation across all 1 revision of this API.