P ProhostAI · Developer portal
Developer portal · Webhooks

Receive ProhostAI events on your endpoint.

Subscribe a URL, verify the HMAC-SHA256 signature on every request, and respond with a 2xx within 10 seconds. ProhostAI retries on 5xx and timeouts with exponential backoff — never on 4xx.

💡 Prefer the API? Manage subscriptions programmatically on the API Reference tab — see POST /v1/webhooks, POST /v1/webhooks/test, and the per-event response examples.

Subscribing

From the ProhostAI dashboard, open Settings → Webhooks, paste your endpoint URL, pick the events you care about, and save. The URL must be https:// — plain HTTP is rejected.

  1. Paste your URL — e.g. https://hooks.example.com/prohost.
  2. Pick your events — subscribe to as many or as few as you like (the full event catalog is below).
  3. Copy the signing secret — it’s shown once on create. Store it in your secrets manager; you’ll need it to verify every request.

On subscription create — and on every POST /v1/webhooks/test call — ProhostAI fires a webhook.verification event so you can confirm signing and reachability end-to-end before any real traffic flows. The verification envelope’s data block is best-effort metadata; key your verification handler on event === "webhook.verification", not on specific data fields.

Request headers

Every delivery carries the headers below. The signature header is the load-bearing one — verify it before parsing the JSON body.

HeaderDescription
x-webhook-event Event type, e.g. reservation.created. Mirrors the event field in the JSON body so you can route on either.
x-webhook-signature HMAC-SHA256 of the raw request body, hex-encoded, prefixed with sha256=. See Signature verification.
x-webhook-delivery rolling out Per-attempt-set idempotency key (ULID) — stable across retries of the same delivery. Use this to dedupe at your receiver.
x-webhook-attempt rolling out 1-indexed retry counter. 1 on the first delivery, 2 on the first retry, and so on.
user-agent Always ProhostAI-Webhook/1.0. Useful for distinguishing live deliveries from your own test traffic in receiver logs.
content-type Always application/json. The body is canonical JSON (UTF-8 bytes; sorted keys) — HMAC is computed over those exact bytes.

The rolling out headers are part of the live contract you can rely on going forward; if your subscription was created before they shipped you may not see them on the very first deliveries. Code your receiver to dedupe by x-webhook-delivery when present and fall back to the JSON body’s id field otherwise — both are stable identifiers ProhostAI never reuses.

Signature verification

ProhostAI signs every delivery with HMAC-SHA256 over the raw request body — the exact bytes on the wire, before your framework parses them as JSON. Verify the signature against those raw bytes, in constant time, and reject any request whose signature doesn’t match.

The signing secret is the value shown to you once on subscription create. Treat it like an API key: store it in a secrets manager and rotate it from the dashboard if it leaks.

Python (Flask / FastAPI / Django)

python import hmac import hashlib def verify_webhook(secret: str, raw_body: bytes, signature_header: str) -> bool: """Verify the X-Webhook-Signature header against the raw request body. Compute HMAC-SHA256 over the EXACT bytes received (not parsed JSON) and compare in constant time. """ if not signature_header.startswith("sha256="): return False expected = hmac.new( secret.encode(), raw_body, hashlib.sha256, ).hexdigest() received = signature_header[len("sha256="):] return hmac.compare_digest(expected, received)

Node.js (Express / Fastify / Cloudflare Workers)

javascript const crypto = require("crypto"); function verifyWebhook(secret, rawBody, signatureHeader) { // rawBody must be Buffer/Uint8Array — verify BEFORE JSON.parse. if (!signatureHeader || !signatureHeader.startsWith("sha256=")) { return false; } const expected = crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex"); const received = signatureHeader.slice("sha256=".length); const a = Buffer.from(expected, "hex"); const b = Buffer.from(received, "hex"); if (a.length !== b.length) return false; return crypto.timingSafeEqual(a, b); }
⚠️ Capture the raw body before parsing. In Express, register express.raw({ type: "application/json" }) on the webhook route so req.body is a Buffer. In FastAPI, read await request.body() before await request.json(). Once your framework parses + reserialises the body, key order and whitespace can change and the signature check will fail.

Retries & idempotency

Respond with any 2xx within 10 seconds and we consider the delivery successful. 5xx, 429, and network / timeout failures are retried; any other 4xx is treated as a permanent client bug and never retried (a misconfigured receiver won’t fix itself).

Backoff is Stripe-style and totals about 31 hours over 7 retries:

retry schedule attempt 1 → fail → wait 5 min attempt 2 → fail → wait 15 min attempt 3 → fail → wait 1 hour attempt 4 → fail → wait 2 hours attempt 5 → fail → wait 4 hours attempt 6 → fail → wait 8 hours attempt 7 → fail → wait 16 hours attempt 8 → give up — delivery is marked failed_terminal

After 24 hours of unbroken 5xx failures — or 5 consecutive terminal retries — ProhostAI auto-pauses the subscription and surfaces it in the dashboard so your team can fix the receiver and resume.

Dedupe with x-webhook-delivery

Retries replay the same envelope — same body, same signature, same x-webhook-delivery. Network blips can also cause a successful delivery to look failed on our side and trigger a retry your receiver already processed. Dedupe by stashing the delivery ID in a short-lived store (Redis, a unique index on a webhook_deliveries table, etc.) and skipping any ID you’ve seen before:

pseudocode delivery_id = headers["x-webhook-delivery"] # ULID, e.g. "01HFXAAA…" event_id = body["id"] # also stable; fallback key = delivery_id or event_id if seen_recently(key): return 200, "already processed" # idempotent ack process(body) mark_seen(key, ttl="48h") return 200

Event catalog 21 events

Every event ProhostAI emits, in the exact envelope shape POST /v1/webhooks can subscribe to. Click any card to expand a JSON sample — the id / event / created_at / schema_version / data envelope is identical across every event type, so receivers can route on event alone.

reservation.created Fired when a new reservation is ingested from any OTA (Airbnb, Hostaway, Guesty, Calry, Hospitable) or created directly in the ProhostAI dashboard. **Not yet emitted in production — wires in PR 4.**
json { "created_at": "2026-04-24T14:05:00Z", "data": { "check_in_at": "2026-05-10T15:00:00Z", "check_out_at": "2026-05-14T11:00:00Z", "created_at": "2026-04-24T14:05:00Z", "currency": "USD", "guest_email": "alex.rivera@example.com", "guest_first_name": "Alex", "guest_id": "22222222-3333-4444-8555-666677778888", "guest_last_name": "Rivera", "guest_phone": "+15551234567", "id": "11111111-2222-4333-8444-555566667777", "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "notes": null, "num_adults": 2, "num_children": 0, "num_infants": 0, "num_nights": 4, "status": "confirmed", "updated_at": "2026-04-24T14:05:00Z" }, "event": "reservation.created", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef111", "schema_version": 1 }
reservation.updated Fired when a reservation's check-in/check-out, guest count, or status changes. Payload is ReservationExternal.
json { "created_at": "2026-04-24T14:10:00Z", "data": { "check_in_at": "2026-05-10T15:00:00Z", "check_out_at": "2026-05-14T11:00:00Z", "created_at": "2026-04-24T14:05:00Z", "currency": "USD", "guest_email": "alex.rivera@example.com", "guest_first_name": "Alex", "guest_id": "22222222-3333-4444-8555-666677778888", "guest_last_name": "Rivera", "guest_phone": "+15551234567", "id": "11111111-2222-4333-8444-555566667777", "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "notes": null, "num_adults": 2, "num_children": 0, "num_infants": 0, "num_nights": 5, "status": "confirmed", "updated_at": "2026-04-24T14:10:00Z" }, "event": "reservation.updated", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef112", "schema_version": 1 }
reservation.cancelled Fired when a reservation transitions into the cancelled status. Payload is ReservationExternal with status: 'cancelled'.
json { "created_at": "2026-04-24T14:15:00Z", "data": { "check_in_at": "2026-05-10T15:00:00Z", "check_out_at": "2026-05-14T11:00:00Z", "created_at": "2026-04-24T14:05:00Z", "currency": "USD", "guest_email": "alex.rivera@example.com", "guest_first_name": "Alex", "guest_id": "22222222-3333-4444-8555-666677778888", "guest_last_name": "Rivera", "guest_phone": "+15551234567", "id": "11111111-2222-4333-8444-555566667777", "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "notes": null, "num_adults": 2, "num_children": 0, "num_infants": 0, "num_nights": 4, "status": "cancelled", "updated_at": "2026-04-24T14:15:00Z" }, "event": "reservation.cancelled", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef113", "schema_version": 1 }
reservation.check_in Fired by the hourly reservation_lifecycle_events Hatchet cron on the calendar day the reservation's check_in_at falls into. Idempotent — fires at most once per reservation. Payload is ReservationExternal plus an event_at field pinning the canonical check-in timestamp.
json { "created_at": "2026-05-10T15:00:00Z", "data": { "check_in_at": "2026-05-10T15:00:00Z", "check_out_at": "2026-05-14T11:00:00Z", "created_at": "2026-04-24T14:05:00Z", "currency": "USD", "event_at": "2026-05-10T15:00:00Z", "guest_email": "alex.rivera@example.com", "guest_first_name": "Alex", "guest_id": "22222222-3333-4444-8555-666677778888", "guest_last_name": "Rivera", "guest_phone": "+15551234567", "id": "11111111-2222-4333-8444-555566667777", "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "notes": null, "num_adults": 2, "num_children": 0, "num_infants": 0, "num_nights": 4, "status": "confirmed", "updated_at": "2026-04-24T14:05:00Z" }, "event": "reservation.check_in", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef121", "schema_version": 1 }
reservation.check_out Fired by the hourly reservation_lifecycle_events Hatchet cron on the calendar day the reservation's check_out_at falls into. Idempotent. Payload mirrors reservation.check_in.
json { "created_at": "2026-05-14T11:00:00Z", "data": { "check_in_at": "2026-05-10T15:00:00Z", "check_out_at": "2026-05-14T11:00:00Z", "created_at": "2026-04-24T14:05:00Z", "currency": "USD", "event_at": "2026-05-14T11:00:00Z", "guest_email": "alex.rivera@example.com", "guest_first_name": "Alex", "guest_id": "22222222-3333-4444-8555-666677778888", "guest_last_name": "Rivera", "guest_phone": "+15551234567", "id": "11111111-2222-4333-8444-555566667777", "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "notes": null, "num_adults": 2, "num_children": 0, "num_infants": 0, "num_nights": 4, "status": "confirmed", "updated_at": "2026-04-24T14:05:00Z" }, "event": "reservation.check_out", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef122", "schema_version": 1 }
message.received Fired when a guest message is ingested from an OTA inbox or SMS channel. **Not yet emitted in production — wires in PR 4.**
json { "created_at": "2026-04-24T14:20:01Z", "data": { "channel": "airbnb", "conversation_id": "44444444-5555-4666-8777-888899990000", "id": "33333333-4444-4555-8666-777788889999", "message": "Hi! What time is check-in?", "sent_at": "2026-04-24T14:20:00Z" }, "event": "message.received", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef114", "schema_version": 1 }
message.sent Fired when a host or autopilot reply is delivered to the guest. Payload is the inline shape from api_public/routers/conversations.py.
json { "created_at": "2026-04-24T14:21:00Z", "data": { "channel": "airbnb", "conversation_id": "44444444-5555-4666-8777-888899990000", "id": "55555555-6666-4777-8888-999900001111", "message": "Check-in is from 3pm onward \u2014 full instructions arrive 24h before arrival.", "sent_at": "2026-04-24T14:20:59Z" }, "event": "message.sent", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef115", "schema_version": 1 }
task.created Fired when a maintenance/ops task is created. Payload is TaskExternal.
json { "created_at": "2026-04-24T14:25:00Z", "data": { "created_at": "2026-04-24T14:25:00Z", "description": "Guest reports the lamp on the right nightstand isn't working.", "due_date": "2026-04-26T17:00:00Z", "id": "66666666-7777-4888-8999-aaaabbbbcccc", "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "priority": "normal", "reservation_id": "11111111-2222-4333-8444-555566667777", "status": "open", "title": "Replace bedroom lightbulb", "updated_at": "2026-04-24T14:25:00Z" }, "event": "task.created", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef116", "schema_version": 1 }
task.updated Fired when a task changes — status transition, assignee, due date, etc. Payload is TaskExternal.
json { "created_at": "2026-04-24T14:30:00Z", "data": { "created_at": "2026-04-24T14:25:00Z", "description": "Guest reports the lamp on the right nightstand isn't working.", "due_date": "2026-04-26T17:00:00Z", "id": "66666666-7777-4888-8999-aaaabbbbcccc", "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "priority": "normal", "reservation_id": "11111111-2222-4333-8444-555566667777", "status": "completed", "title": "Replace bedroom lightbulb", "updated_at": "2026-04-24T14:30:00Z" }, "event": "task.updated", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef117", "schema_version": 1 }
cleaning.created Fired when a turnover cleaning is scheduled. Payload is CleaningExternal.
json { "created_at": "2026-04-24T14:35:00Z", "data": { "assignee_id": "99999999-aaaa-4bbb-8ccc-ddddeeeeffff", "created_at": "2026-04-24T14:35:00Z", "description": "Standard turnover cleaning.", "id": "88888888-9999-4aaa-8bbb-ccccddddeeee", "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "reservation_id": "11111111-2222-4333-8444-555566667777", "scheduled_ends_at": "2026-05-15T14:00:00Z", "scheduled_starts_at": "2026-05-15T11:00:00Z", "status": "not_started", "title": "Post-checkout turnover", "type": "turnover", "updated_at": "2026-04-24T14:35:00Z" }, "event": "cleaning.created", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef118", "schema_version": 1 }
cleaning.updated Fired on cleaning status transitions and reassignment. Payload is CleaningExternal.
json { "created_at": "2026-04-24T14:40:00Z", "data": { "assignee_id": "99999999-aaaa-4bbb-8ccc-ddddeeeeffff", "created_at": "2026-04-24T14:35:00Z", "description": "Standard turnover cleaning.", "id": "88888888-9999-4aaa-8bbb-ccccddddeeee", "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "reservation_id": "11111111-2222-4333-8444-555566667777", "scheduled_ends_at": "2026-05-15T14:00:00Z", "scheduled_starts_at": "2026-05-15T11:00:00Z", "status": "in_progress", "title": "Post-checkout turnover", "type": "turnover", "updated_at": "2026-04-24T14:40:00Z" }, "event": "cleaning.updated", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef119", "schema_version": 1 }
cleaning.completed Fires only on the not-completed → completed transition for a cleaning. Distinct from cleaning.updated (every-change emitter); both can fire for the same write — subscribe to cleaning.completed only if you want the completion signal alone. Payload is CleaningExternal.
json { "created_at": "2026-04-24T14:42:00Z", "data": { "assignee_id": "99999999-aaaa-4bbb-8ccc-ddddeeeeffff", "created_at": "2026-04-24T14:35:00Z", "description": "Standard turnover cleaning.", "id": "88888888-9999-4aaa-8bbb-ccccddddeeee", "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "reservation_id": "11111111-2222-4333-8444-555566667777", "scheduled_ends_at": "2026-05-15T14:00:00Z", "scheduled_starts_at": "2026-05-15T11:00:00Z", "status": "completed", "title": "Post-checkout turnover", "type": "turnover", "updated_at": "2026-04-24T14:42:00Z" }, "event": "cleaning.completed", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef120", "schema_version": 1 }
guest.created Fired the first time a guest profile is created — typically alongside their first reservation. **Not yet emitted in production — wires in PR 4.**
json { "created_at": "2026-04-24T14:45:00Z", "data": { "created_at": "2026-04-24T14:45:00Z", "email": "alex.rivera@example.com", "first_name": "Alex", "id": "22222222-3333-4444-8555-666677778888", "last_name": "Rivera", "phone": "+15551234567" }, "event": "guest.created", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef11a", "schema_version": 1 }
review.received Fired when a guest review is ingested from any OTA (Airbnb, Hostaway, Hospitable, Guesty, etc.) — one event per upserted reviews row. Payload mirrors the Review model with category ratings flattened into a category_ratings sub-object.
json { "created_at": "2026-04-26T10:00:00Z", "data": { "category_ratings": { "accuracy": 5.0, "checkin": 5.0, "cleanliness": 5.0, "communication": 5.0, "location": 5.0, "value": 5.0 }, "created_at": "2026-04-26T10:00:00Z", "id": "01HFXAAAAAAAAAAAAAAAAAAA00", "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "private_review": null, "public_review": "Great stay \u2014 would book again.", "published_at": "2026-04-26T10:00:00Z", "rating": 5.0, "reservation_id": "11111111-2222-4333-8444-555566667777", "reviewer_role": "guest", "source_review_id": "abnb-review-1234", "stars": 5 }, "event": "review.received", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef124", "schema_version": 1 }
review.replied Fired when an AI-generated host reply has been successfully posted back to the OTA via the auto-review publish path. Payload includes the body of the reply and the source review identifier.
json { "created_at": "2026-04-27T09:00:00Z", "data": { "auto_review_id": "01HFXBBBBBBBBBBBBBBBBBBB00", "is_reviewee_recommended": true, "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "private_feedback": null, "public_review": "Thanks for staying with us \u2014 you were a wonderful guest!", "reservation_id": "11111111-2222-4333-8444-555566667777", "source_review_id": "abnb-review-1234", "submitted_at": "2026-04-27T09:00:00Z" }, "event": "review.replied", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef125", "schema_version": 1 }
autopilot.escalation Fired when autopilot hands a draft reply off to human review — reason is one of category_not_enabled / low_confidence / negative_sentiment. Payload reuses AutopilotEscalationNotification plus webhook-only fields (reason, sentiment_score, confidence_overall, suggestion_id, trigger_message_id, drafted_reply). Common pattern: route to Slack/PagerDuty for oncall handoff.
json { "created_at": "2026-04-24T14:55:00Z", "data": { "account_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "category_rule_enabled": true, "confidence_overall": 0.74, "conversation_id": "44444444-5555-4666-8777-888899990000", "drafted_reply": "I'm sorry to hear that \u2014 let me look into this and get back to you within the hour.", "guest_full_name": "Alex Rivera", "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "listing_title": "Sunny Loft Downtown", "message_category": "complaint", "reason": "negative_sentiment", "sentiment_score": 0.18, "suggestion_id": "cccccccc-dddd-4eee-8fff-000011112222", "trigger_message_id": "33333333-4444-4555-8666-777788889999" }, "event": "autopilot.escalation", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef11c", "schema_version": 1 }
autopilot.reply_sent Inverse branch of autopilot.escalation — fired when autopilot accepts a draft reply and schedules it for delivery. Payload includes the eventual scheduled_message_id + scheduled_at so receivers can correlate the eventual message.sent event. Useful for AI message audit trails.
json { "created_at": "2026-04-24T14:56:00Z", "data": { "account_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "confidence_overall": 0.91, "conversation_id": "44444444-5555-4666-8777-888899990000", "drafted_reply": "Check-in is from 3pm onward \u2014 full instructions arrive 24h before arrival.", "guest_full_name": "Alex Rivera", "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "listing_title": "Sunny Loft Downtown", "message_category": "checkin_question", "scheduled_at": "2026-04-24T14:56:30Z", "scheduled_message_id": "01HFXYZABCDEF0123456789ABCD", "sentiment_score": 0.62, "suggestion_id": "cccccccc-dddd-4eee-8fff-000011112222", "trigger_message_id": "33333333-4444-4555-8666-777788889999" }, "event": "autopilot.reply_sent", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef123", "schema_version": 1 }
smart_device.alert Fired by the Seam smart-device ingest path for high-signal alert events (currently noise_sensor.noise_threshold_triggered and lock.access_denied). The alert_type field on the payload echoes the upstream Seam event type so receivers can branch.
json { "created_at": "2026-04-28T22:30:00Z", "data": { "alert_type": "noise_sensor.noise_threshold_triggered", "device_id": "01HFXDDDDDDDDDDDDDDDDDDD00", "device_type": "noise_sensor", "display_name": "Living Room Minut", "occurred_at": "2026-04-28T22:30:00Z", "provider": "seam", "provider_device_id": "seam-device-xyz", "raw_payload": { "device_id": "seam-device-xyz", "noise_level_decibels": 92, "noise_level_threshold_decibels": 85 } }, "event": "smart_device.alert", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef126", "schema_version": 1 }
customer_feedback.submitted Fired when a host or guest submits feedback through the in-app form, the MCP tool, or the AI chat bug-report tool. Payload mirrors the CustomerFeedback row.
json { "created_at": "2026-04-24T16:00:00Z", "data": { "ai_chat_session_id": null, "created_at": "2026-04-24T16:00:00Z", "description": "Pages 2+ skip the last item from the previous page.", "id": "01HFXEEEEEEEEEEEEEEEEEEE00", "source": "api", "status": "submitted", "title": "Reservation list pagination off-by-one", "type": "bug_report", "user_id": "99999999-aaaa-4bbb-8ccc-ddddeeeeffff" }, "event": "customer_feedback.submitted", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef127", "schema_version": 1 }
expense.created Fired when a new expense is created via the public or internal API. Payload mirrors the Expense row — amount is a decimal string for precision.
json { "created_at": "2026-04-24T16:30:00Z", "data": { "amount": "12.50", "category_id": null, "cleaning_id": null, "created_at": "2026-04-24T16:30:00Z", "currency": "USD", "date": "2026-04-24", "description": "Replaced bedroom lamp.", "id": "01HFXFFFFFFFFFFFFFFFFFFF00", "listing_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeffff0000", "name": "Lightbulb replacement", "payment_payee_id": null, "payment_status": "unpaid", "payment_type": "manual", "reservation_id": "11111111-2222-4333-8444-555566667777", "task_id": null, "updated_at": "2026-04-24T16:30:00Z" }, "event": "expense.created", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef128", "schema_version": 1 }
webhook.verification Fired on first subscription creation and on every POST /webhooks/test call. Used to verify HMAC signing + endpoint reachability. data is best-effort metadata — receivers should key only on event == 'webhook.verification', not on specific data fields.
json { "created_at": "2026-04-24T14:50:00Z", "data": { "message": "Webhook subscription created \u2014 this is a verification ping.", "subscription_id": "e5f6a7b8-c9d0-4123-8456-7890abcdef12", "url": "https://example.com/hooks/prohost" }, "event": "webhook.verification", "id": "f6a7b8c9-d0e1-4234-8567-890abcdef11b", "schema_version": 1 }

Recipes

How to wire a ProhostAI subscription into the three destinations we get asked about most. Click a tab to expand.

Zapier’s Catch Hook trigger ingests ProhostAI webhooks directly — no glue code required to land the event. Catch Hook does not verify HMAC signatures, though, so anyone who learns the URL can post arbitrary events into your Zap. Either accept that risk for low-value automation, or add a Code by Zapier step that verifies before branching.

  1. Create the Zap — pick Webhooks by Zapier → Catch Hook as the trigger. Copy the https://hooks.zapier.com/hooks/catch/… URL.
  2. Subscribe in ProhostAI — paste that URL into Settings → Webhooks, pick the events you want, save.
  3. Sample the right schema — in ProhostAI, fire Send test on one of the events you actually subscribed to so Zapier samples the real shape. The auto-fired webhook.verification event has a generic stub body that won’t match any real event — if Zapier samples that, your downstream steps see the wrong fields.
  4. (Optional) Verify the signature — HMAC verification needs the raw request bytes, but standard Catch Hook parses the JSON body and discards the raw payload — raw_body is not exposed. To verify, switch the trigger to Webhooks by Zapier → Catch Raw Hook (it surfaces raw_body and the request headers) and re-parse the JSON yourself in a Code by Zapier → Run Python step. Downstream Zap steps then read fields off the dict you build, not the trigger.
    python # Trigger: Webhooks by Zapier → Catch Raw Hook import hmac, hashlib, json secret = "whsec_your_signing_secret" # set in Zapier env raw_body = input_data["raw_body"] # only available from Catch Raw Hook sig = input_data["x_webhook_signature"].removeprefix("sha256=") expected = hmac.new(secret.encode(), raw_body.encode(), hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, sig): raise ValueError("Invalid signature — drop") # Re-parse so downstream steps can map data.* fields event = json.loads(raw_body) output = {"verified": True, "event": event["event"], "data": event["data"]}

    Sticking with Catch Hook? Skip this step — HMAC verification isn’t possible without the raw bytes. Anyone who learns the Zap URL can post arbitrary events; accept that for low-value automation only.

Next steps

Need to manage subscriptions programmatically? Every dashboard action has an API equivalent — see POST /v1/webhooks, POST /v1/webhooks/test, and the per-event response examples on the API Reference tab.