Webhooks & events
Get called when something happens — deal won, contact replied, quote accepted, task overdue… — with signed, retried deliveries; plus REST hooks for Zapier, n8n and Make, and the event log.
Every change in Meetzy is an event (the full list is in the reference). A webhook subscribes a URL to events — exact types, a whole family (deal.*) or everything (*). Webhook endpoints need a key with the webhooks scope.
/webhooksList webhooksPOST/webhooksCreate a webhookPATCH/webhooks/{id}Update a webhookDELETE/webhooks/{id}Delete a webhookPOST/webhooks/{id}/testSend a test deliveryGET/webhooks/{id}/deliveriesDelivery logPOST/webhooks/{id}/deliveries/{deliveryId}/redeliverResend a deliveryPOST/hooksSubscribe a REST hook (Zapier, n8n, Make)DELETE/hooks/{id}Unsubscribe a REST hookGET/hooks/sampleSample payload for an eventGET/eventsEvent log/webhooksList webhooks
Every webhook with its URL, events, whether it is active, the last status code and delivery time, and consecutive failures.
curl "https://meetzy.me/api/v1/webhooks" \ -H "Authorization: Bearer $MEETZY_API_KEY"
const res = await fetch("https://meetzy.me/api/v1/webhooks", {
headers: { Authorization: `Bearer ${process.env.MEETZY_API_KEY}` },
});
const data = await res.json();import os, requests
res = requests.get(
"https://meetzy.me/api/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['MEETZY_API_KEY']}"},
)
data = res.json(){
"webhooks": [
{
"id": "c39ea9df-9ff5-4345-af78-cfd7161a310e",
"user_from": "acme",
"url": "https://hooks.acme.com/meetzy",
"events": [
"deal.won"
],
"secret": "whsec_8f3c…",
"description": null,
"source": "api",
"active": true,
"failure_count": 0,
"last_status": null,
"last_delivery_at": null,
"created_by": "olivia@acme.com",
"created_at": "2026-10-07T13:23:46.551452+00:00"
},
{
"id": "03010268-da43-436e-a917-0eafb2bcfe57",
"user_from": "acme",
"url": "https://hooks.zapier.com/hooks/standard/123/abc",
"events": [
"contact.created"
],
"secret": "whsec_8f3c…",
"description": "REST hook",
"source": "rest_hook",
"active": true,
"failure_count": 4,
"last_status": 405,
"last_delivery_at": "2026-10-07T13:22:39.73+00:00",
"created_by": "olivia@acme.com",
"created_at": "2026-10-07T13:09:54.218681+00:00"
}
]
}/webhooksCreate a webhook
The answer contains the secret used to sign deliveries — store it. The URL must be https. Send a test with POST /webhooks/{id}/test.
Body
urlstringrequired- Your endpoint (
https://…). eventsstring[]- Event types,
family.*, or["*"](default). descriptionstring- What it is for.
secretstring- Your own signing secret (default: generated).
curl -X POST "https://meetzy.me/api/v1/webhooks" \ -H "Authorization: Bearer $MEETZY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://hooks.acme.com/meetzy", "events": [ "deal.won", "quote.accepted", "contact.*" ], "description": "ERP order creation" }'
const res = await fetch("https://meetzy.me/api/v1/webhooks", { method: "POST", headers: { Authorization: `Bearer ${process.env.MEETZY_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ "url": "https://hooks.acme.com/meetzy", "events": [ "deal.won", "quote.accepted", "contact.*" ], "description": "ERP order creation" }), }); const data = await res.json();
import os, requests
res = requests.post(
"https://meetzy.me/api/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['MEETZY_API_KEY']}"},
json={
"url": "https://hooks.acme.com/meetzy",
"events": ["deal.won", "quote.accepted", "contact.*"],
"description": "ERP order creation"
},
)
data = res.json(){
"id": "c39ea9df-9ff5-4345-af78-cfd7161a310e",
"user_from": "acme",
"url": "https://hooks.acme.com/meetzy",
"events": [
"deal.won"
],
"secret": "whsec_8f3c…",
"description": null,
"source": "api",
"active": true,
"failure_count": 0,
"last_status": null,
"last_delivery_at": null,
"created_by": "olivia@acme.com",
"created_at": "2026-10-07T13:23:46.551452+00:00"
}/webhooks/{id}Update a webhook
Change the URL, the events, the description, or switch it on and off. Re-activating resets the failure counter.
Path parameters
idstringrequired- Id of the record (UUID).
Body
urlstring- Endpoint.
eventsstring[]- Events.
descriptionstring- Description.
activeboolean- On or off.
Returns the webhook.
curl -X PATCH "https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc" \ -H "Authorization: Bearer $MEETZY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "active": true }'
const res = await fetch("https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc", { method: "PATCH", headers: { Authorization: `Bearer ${process.env.MEETZY_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ "active": true }), }); const data = await res.json();
import os, requests
res = requests.patch(
"https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc",
headers={"Authorization": f"Bearer {os.environ['MEETZY_API_KEY']}"},
json={
"active": True
},
)
data = res.json()See the notes on the left.
/webhooks/{id}Delete a webhook
Path parameters
idstringrequired- Id of the record (UUID).
curl -X DELETE "https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc" \ -H "Authorization: Bearer $MEETZY_API_KEY"
const res = await fetch("https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.MEETZY_API_KEY}` }, }); const data = await res.json();
import os, requests
res = requests.delete(
"https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc",
headers={"Authorization": f"Bearer {os.environ['MEETZY_API_KEY']}"},
)
data = res.json(){
"deleted": true
}/webhooks/{id}/testSend a test delivery
Sends a sample payload ("test": true) now and returns what your endpoint answered. Tests are not retried.
Path parameters
idstringrequired- Id of the record (UUID).
curl -X POST "https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc/test" \ -H "Authorization: Bearer $MEETZY_API_KEY"
const res = await fetch("https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc/test", { method: "POST", headers: { Authorization: `Bearer ${process.env.MEETZY_API_KEY}` }, }); const data = await res.json();
import os, requests
res = requests.post(
"https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc/test",
headers={"Authorization": f"Bearer {os.environ['MEETZY_API_KEY']}"},
)
data = res.json(){
"ok": true,
"status": 200,
"duration_ms": 182,
"response": "ok",
"attempt": 1,
"next_retry_at": null
}/webhooks/{id}/deliveriesDelivery log
The latest deliveries: event, status code, duration, your response (first 500 characters), attempt number and next retry.
Path parameters
idstringrequired- Id of the record (UUID).
Up to the last 50 deliveries.
curl "https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc/deliveries" \ -H "Authorization: Bearer $MEETZY_API_KEY"
const res = await fetch("https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc/deliveries", {
headers: { Authorization: `Bearer ${process.env.MEETZY_API_KEY}` },
});
const data = await res.json();import os, requests
res = requests.get(
"https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc/deliveries",
headers={"Authorization": f"Bearer {os.environ['MEETZY_API_KEY']}"},
)
data = res.json(){
"deliveries": [
{
"id": "f1e2d3c4-b5a6-4789-8a7b-6c5d4e3f2a1b",
"webhook_id": "4f8e2b1a-9c3d-4e5f-8a7b-6c5d4e3f2a10",
"event_id": 3307,
"event_type": "deal.won",
"status_code": 200,
"ok": true,
"duration_ms": 214,
"response": "ok",
"payload": {
"id": "evt_3307",
"type": "deal.won",
"created_at": "2026-10-07T10:02:10Z",
"organization": "acme",
"entity": {
"type": "deal",
"id": "5eecc043-d70d-4b94-a604-7e1a35d92edf"
},
"data": {
"record": {
"id": "5eecc043-d70d-4b94-a604-7e1a35d92edf",
"name": "Payfit — rollout",
"amount": 12000,
"status": "won"
}
},
"test": false
},
"attempt": 1,
"next_retry_at": null,
"created_at": "2026-10-07T10:02:11Z"
}
]
}/webhooks/{id}/deliveries/{deliveryId}/redeliverResend a delivery
Sends the same payload again (same delivery id, next attempt number).
Path parameters
idstringrequired- Id of the record (UUID).
deliveryIdstringrequired- Id of the delivery (from the delivery log).
curl -X POST "https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc/deliveries/f1e2d3c4-b5a6-4789-8a7b-6c5d4e3f2a1b/redeliver" \ -H "Authorization: Bearer $MEETZY_API_KEY"
const res = await fetch("https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc/deliveries/f1e2d3c4-b5a6-4789-8a7b-6c5d4e3f2a1b/redeliver", { method: "POST", headers: { Authorization: `Bearer ${process.env.MEETZY_API_KEY}` }, }); const data = await res.json();
import os, requests
res = requests.post(
"https://meetzy.me/api/v1/webhooks/e43414d6-f56f-42be-a64c-a836d6015efc/deliveries/f1e2d3c4-b5a6-4789-8a7b-6c5d4e3f2a1b/redeliver",
headers={"Authorization": f"Bearer {os.environ['MEETZY_API_KEY']}"},
)
data = res.json(){
"ok": true,
"status": 200,
"duration_ms": 160,
"response": "ok",
"attempt": 2,
"next_retry_at": null
}/hooksSubscribe a REST hook (Zapier, n8n, Make)
The subscribe call of the REST-hook pattern used by Zapier and n8n triggers: creates a webhook for one event (or several) and returns its id and secret.
Body
target_urlstringrequired- URL to call (also
urlorhookUrl). eventstring- One event type.
eventsstring[]- Or several.
descriptionstring- Label.
curl -X POST "https://meetzy.me/api/v1/hooks" \ -H "Authorization: Bearer $MEETZY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "target_url": "https://hooks.zapier.com/hooks/standard/123/abc", "event": "contact.created" }'
const res = await fetch("https://meetzy.me/api/v1/hooks", { method: "POST", headers: { Authorization: `Bearer ${process.env.MEETZY_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ "target_url": "https://hooks.zapier.com/hooks/standard/123/abc", "event": "contact.created" }), }); const data = await res.json();
import os, requests
res = requests.post(
"https://meetzy.me/api/v1/hooks",
headers={"Authorization": f"Bearer {os.environ['MEETZY_API_KEY']}"},
json={
"target_url": "https://hooks.zapier.com/hooks/standard/123/abc",
"event": "contact.created"
},
)
data = res.json(){
"id": "23567d4a-13b9-4120-a41b-0e66036a6c47",
"secret": "whsec_8f3c…",
"events": [
"contact.created"
]
}/hooks/{id}Unsubscribe a REST hook
Path parameters
idstringrequired- Id of the record (UUID).
curl -X DELETE "https://meetzy.me/api/v1/hooks/e43414d6-f56f-42be-a64c-a836d6015efc" \ -H "Authorization: Bearer $MEETZY_API_KEY"
const res = await fetch("https://meetzy.me/api/v1/hooks/e43414d6-f56f-42be-a64c-a836d6015efc", { method: "DELETE", headers: { Authorization: `Bearer ${process.env.MEETZY_API_KEY}` }, }); const data = await res.json();
import os, requests
res = requests.delete(
"https://meetzy.me/api/v1/hooks/e43414d6-f56f-42be-a64c-a836d6015efc",
headers={"Authorization": f"Bearer {os.environ['MEETZY_API_KEY']}"},
)
data = res.json(){
"deleted": true
}/hooks/sampleSample payload for an event
An array with one example delivery for an event, built from your latest matching record — what Zapier / n8n show while you map fields.
Query parameters
eventstring- Event type. Default
contact.created.
curl "https://meetzy.me/api/v1/hooks/sample?event=deal.won" \ -H "Authorization: Bearer $MEETZY_API_KEY"
const res = await fetch("https://meetzy.me/api/v1/hooks/sample?event=deal.won", {
headers: { Authorization: `Bearer ${process.env.MEETZY_API_KEY}` },
});
const data = await res.json();import os, requests
res = requests.get(
"https://meetzy.me/api/v1/hooks/sample?event=deal.won",
headers={"Authorization": f"Bearer {os.environ['MEETZY_API_KEY']}"},
)
data = res.json()[
{
"id": "evt_sample",
"type": "deal.won",
"created_at": "2026-10-07T13:23:49.451Z",
"entity": {
"type": "deal",
"id": "d74acfa5-9877-4d7f-a369-3a505bac042a"
},
"data": {
"record": {
"id": "d74acfa5-9877-4d7f-a369-3a505bac042a",
"user_from": "acme",
"name": "Payfit — rollout",
"pipeline_id": "cacafee6-6c63-407f-a948-5d6570c20143",
"stage_id": "proposal",
"status": "open",
"amount": 12000,
"currency": "EUR",
"probability": null,
"close_date": "2026-11-06",
"owner_email": "olivia@acme.com",
"company_id": "166c436d-a874-4730-a0d8-c8d7fa090268",
"primary_lead_id": "8028ff76-1da4-451f-a0e7-dd91db772c20",
"priority": "medium",
"deal_type": "new_business",
"source": null,
"recurring": false,
"mrr": null,
"next_step": null,
"next_step_date": null,
"description": null,
"lost_reason": null,
"won_reason": null,
"competitors": [],
"tags": [],
"custom": {},
"stage_entered_at": "2026-10-07T13:22:51.622676+00:00",
"stage_history": [
{
"at": "2026-10-07T13:22:51.622676+00:00",
"to": "proposal",
"from": "qualification",
"days_in_stage": 0.000041483657407407404
}
],
"closed_at": null,
"last_activity_at": null,
"created_by": "olivia@acme.com",
"created_at": "2026-10-07T13:22:48.038488+00:00",
"updated_at": "2026-10-07T13:22:53.559344+00:00"
}
}
}
]/eventsEvent log
The latest events of the workspace, newest first: type, record, payload, when it happened and when workflows and webhooks processed it. Handy to debug or to poll instead of receiving webhooks.
Query parameters
typestring- One event type.
limitinteger- Default 50.
curl "https://meetzy.me/api/v1/events?type=deal.won&limit=20" \ -H "Authorization: Bearer $MEETZY_API_KEY"
const res = await fetch("https://meetzy.me/api/v1/events?type=deal.won&limit=20", {
headers: { Authorization: `Bearer ${process.env.MEETZY_API_KEY}` },
});
const data = await res.json();import os, requests
res = requests.get(
"https://meetzy.me/api/v1/events?type=deal.won&limit=20",
headers={"Authorization": f"Bearer {os.environ['MEETZY_API_KEY']}"},
)
data = res.json(){
"events": [
{
"id": 3307,
"type": "sequence.exited",
"entity_type": "contact",
"entity_id": "8028ff76-1da4-451f-a0e7-dd91db772c20",
"payload": {
"reason": "manual",
"sequence_id": "8ff666e1-a95c-4b4a-ad4b-250fa13794be",
"enrollment_id": "7be8416d-3e67-4ec7-a1ed-c590f7d02299"
},
"created_at": "2026-10-07T13:23:11.315289+00:00",
"processed_at": "2026-10-07T13:23:12.064+00:00"
},
{
"id": 3306,
"type": "task.created",
"entity_type": "task",
"entity_id": "e3ecc019-a3ac-4a49-a642-3fbec133aa2e",
"payload": {
"id": "e3ecc019-a3ac-4a49-a642-3fbec133aa2e",
"type": "todo",
"owner": "olivia@acme.com",
"title": "Intro call",
"due_at": "2026-10-07T17:23:10.013+00:00",
"source": "sequence",
"deal_id": null,
"lead_id": "8028ff76-1da4-451f-a0e7-dd91db772c20"
},
"created_at": "2026-10-07T13:23:10.037612+00:00",
"processed_at": "2026-10-07T13:23:12.064+00:00"
}
]
}What a delivery looks like
A POST with a JSON body. data summarises the record at the time of the event (name, owner, stage, amount…) with what changed — from / to for moves, changed for property changes — and data.record is the full record as it is now (contact, deal, company, task or quote). Answer with any 2xx within 10 seconds.
{
"id": "evt_3307",
"type": "deal.won",
"created_at": "2026-10-07T10:02:10Z",
"organization": "acme",
"entity": {
"type": "deal",
"id": "5eecc043-d70d-4b94-a604-7e1a35d92edf"
},
"data": {
"id": "5eecc043-d70d-4b94-a604-7e1a35d92edf",
"name": "Payfit — rollout",
"owner": "olivia@acme.com",
"from": "negotiation",
"to": "won",
"stage": "won",
"status": "won",
"amount": 12000,
"currency": "EUR",
"close_date": "2026-11-06",
"company_id": "3cdc3e21-931a-4e8b-a189-3cab5dfbd636",
"pipeline_id": "cacafee6-6c63-407f-a948-5d6570c20143",
"record": {
"id": "5eecc043-d70d-4b94-a604-7e1a35d92edf",
"name": "Payfit — rollout",
"amount": 12000,
"currency": "EUR",
"status": "won",
"stage_id": "won",
"owner_email": "olivia@acme.com",
"company_id": "3cdc3e21-931a-4e8b-a189-3cab5dfbd636",
"primary_lead_id": "e43414d6-f56f-42be-a64c-a836d6015efc",
"closed_at": "2026-10-07T10:02:09Z",
"…": "…"
}
},
"test": false
}| Header | Value |
|---|---|
X-Meetzy-Event | The event type, e.g. deal.won |
X-Meetzy-Delivery | Id of the event — the same on every retry: use it to ignore duplicates |
X-Meetzy-Attempt | 1 for the first try, then 2, 3… |
X-Meetzy-Timestamp | Unix time (seconds) of this attempt |
X-Meetzy-Signature | sha256=HMAC_SHA256(secret, timestamp + "." + raw body), in hex |
User-Agent | Meetzy-Webhooks/1.0 |
Verify the signature
Compute the HMAC of {timestamp}.{raw body} with the webhook’s secret and compare it, in constant time, to the header. Reject old timestamps to stop replays. Use the raw body exactly as received — not re-serialised JSON.
import crypto from "node:crypto"; // Express: app.post("/meetzy", express.raw({ type: "application/json" }), handler) function verify(req, secret) { const ts = req.header("X-Meetzy-Timestamp"); const sig = req.header("X-Meetzy-Signature"); // "sha256=…" const body = req.body.toString("utf8"); // the raw body const expected = "sha256=" + crypto.createHmac("sha256", secret).update(`${ts}.${body}`).digest("hex"); const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300; // 5 minutes return fresh && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)); }
import hmac, hashlib, time
def verify(headers, raw_body: bytes, secret: str) -> bool:
ts = headers["X-Meetzy-Timestamp"]
expected = "sha256=" + hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(ts)) < 300
return fresh and hmac.compare_digest(expected, headers["X-Meetzy-Signature"])<?php $ts = $_SERVER['HTTP_X_MEETZY_TIMESTAMP']; $raw = file_get_contents('php://input'); $expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret); $ok = abs(time() - (int) $ts) < 300 && hash_equals($expected, $_SERVER['HTTP_X_MEETZY_SIGNATURE']);
Retries
A delivery that fails with a network error, a 5xx, 408 or 429 is retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours (6 attempts in all). Other 4xx are not retried — your endpoint refused the payload. After 25 failures in a row the webhook is switched off (active: false); turn it back on with PATCH /webhooks/{id}. Every attempt is in the delivery log and can be resent.
Event types
| Event | When | Group |
|---|---|---|
contact.created | Contact created | Contacts |
contact.stage_changed | Contact pipeline stage changed | Contacts |
contact.status_changed | Contact status changed | Contacts |
contact.lifecycle_changed | Lifecycle stage changed | Contacts |
contact.score_changed | Lead score changed | Contacts |
contact.owner_changed | Contact owner changed | Contacts |
contact.replied | Contact replied (email, LinkedIn, WhatsApp) | Contacts |
contact.email_sent | You emailed the contact (any mailbox: Outlook, Gmail…) | Contacts |
contact.email_received | The contact emailed you (any mailbox) | Contacts |
contact.message_received | WhatsApp / LinkedIn message received | Contacts |
contact.message_sent | You sent a WhatsApp / LinkedIn message (any device) | Contacts |
contact.email_opened | Email opened | Contacts |
contact.email_clicked | Email link clicked | Contacts |
contact.website_visit | Visited the website | Contacts |
contact.meeting_booked | Meeting booked | Contacts |
contact.linkedin_connected | LinkedIn invitation accepted | Contacts |
contact.tags_changed | Labels / tags changed | Contacts |
contact.note_added | Note added | Contacts |
contact.property_changed | Contact property changed | Contacts |
contact.deleted | Contact deleted | Contacts |
deal.created | Deal created | Deals |
deal.stage_changed | Deal stage changed | Deals |
deal.won | Deal won | Deals |
deal.lost | Deal lost | Deals |
deal.reopened | Deal reopened | Deals |
deal.amount_changed | Deal amount changed | Deals |
deal.owner_changed | Deal owner changed | Deals |
deal.rotting | Deal stuck in stage too long | Deals |
deal.overdue | Deal past its close date | Deals |
deal.property_changed | Deal property changed | Deals |
company.created | Company created | Companies |
company.lifecycle_changed | Company lifecycle changed | Companies |
task.created | Task created | Tasks |
task.completed | Task completed | Tasks |
task.overdue | Task overdue | Tasks |
quote.sent | Quote sent | Quotes |
quote.viewed | Quote viewed | Quotes |
quote.accepted | Quote accepted | Quotes |
quote.declined | Quote declined | Quotes |
sequence.enrolled | Enrolled in sequence | Sequences |
sequence.completed | Sequence completed | Sequences |
sequence.exited | Exited sequence | Sequences |
email.sent | Email sent (campaign or workflow) | |
email.opened | Email opened | |
email.clicked | Link clicked in an email | |
email.failed | Email could not be sent | |
contact.unsubscribed | Contact unsubscribed | |
webhook.received | Inbound webhook (n8n, Zapier, forms…) | External |
form.submitted | Form submitted | External |
workflow.enrolled | Enrolled manually (CRM, another workflow, API, Claude) | External |