Inbound webhooks
Register endpoints that trigger tasks or emit events, with HMAC signing.
An inbound webhook is an endpoint Meteor exposes so a third party can POST to it. When an event arrives, Meteor can trigger a task or emit an internal event. It needs the webhooks:manage scope.
These are inbound webhooks (third party → Meteor). If what you want is for Meteor to notify you when something happens (
run.completed, etc.), jump to Outbound events below.
Create a webhook
const wh = await met.webhooks.create({
name: 'Lead intake',
binding_type: 'generic', // 'task' runs a task; 'generic' only emits an event
verifier_type: 'hmac_sha256', // 'none' for unsigned
});
console.log(wh.url); // the URL the third party points at
console.log(wh.secret); // ⚠️ shown ONCE only
Store the secret the moment you receive it: it is never shown again. With it you verify that each delivery really came from your integration.
Verify the signature
The third party signs the body with the secret (HMAC-SHA256) and sends the signature in a header. On Meteor's side, verification is automatic if the webhook is hmac_sha256.
Rotate the secret
If you suspect it leaked:
const rotated = await met.webhooks.rotateSecret(wh.id);
console.log(rotated.secret); // new secret; invalidates the previous one
See deliveries
const events = await met.webhooks.events(wh.id, { limit: 20 });
const sample = await met.webhooks.sample(wh.id); // last payload received
sample is handy for setting up the payload's field mapping.
Connect a flow
A generic webhook can trigger a flow when it receives an event (connectFlow in the API). That way an external POST starts a full automation inside Meteor.
Outbound events (Meteor → your server)
An outbound event is Meteor POSTing to *you* when something happens in your workspace — for example, when a Run finishes. You register an endpoint, choose which events you want, and Meteor delivers each one signed with HMAC so you can verify it came from Meteor.
Register an endpoint
You register the endpoint from your panel: Developers → Activity → Webhooks. You choose your server's URL and which events you want (or * for all of them). On creation, Meteor shows you the signing secret (whsec_...) once only — store it, you need it to verify every delivery.
Event catalog
There are 26. The scope column is the permission a key must carry to receive that event: the same one you would read that resource with through the API.
| Event | When it fires | Scope |
|---|---|---|
run.completed | A Run finished successfully. | runs:read |
run.failed | A Run failed. | runs:read |
channel.run.completed | The Met answered a channel message. | runs:read |
channel.run.failed | The Met failed to answer a channel message. | runs:read |
contact.created | A contact was created. | contacts:read |
contact.updated | A contact was updated. | contacts:read |
contact.message.received | A message from a contact came in through a channel (WhatsApp, etc.). | conversations:read |
conversation.handoff | A conversation moved to a human operator. | conversations:read |
task.completed | An agentic task finished. | tasks:read |
billing.threshold | An API key's Energy spend crossed a threshold of its budget (50/80/100%). | billing:read |
app.authorized | A workspace authorized your OAuth app (new grant). | integrations:read |
app.revoked | A workspace revoked your OAuth app's access (grant revoked). | integrations:read |
snapshot.published | One of your templates was approved and published to the marketplace. | snapshots:read |
snapshot.install.completed | A template install finished. | snapshots:read |
snapshot.install.failed | A template install failed. | snapshots:read |
conversion.sent | A conversion was emitted successfully to Meta (CAPI). | conversions:read |
conversion.discarded | A proposed conversion was discarded by the gate (the reason is included). | conversions:read |
automation.run.failed | An automation failed to run. | automations:read |
contact.message.sent | A message went out to a contact, whether the Met or a person wrote it. | conversations:read |
contact.deleted | A contact was deleted. | contacts:read |
broadcast.completed | A broadcast finished sending (includes how many went out and how many failed). | channels:read |
call.missed | A call went unanswered. | conversations:read |
billing.energy.low | The Energy balance crossed below the low threshold. It fires on the crossing, not on every execution. | billing:read |
billing.energy.depleted | The Energy balance ran out. | billing:read |
billing.energy.recharged | An Energy top-up came in (purchase, auto-recharge, or a partner credit). | billing:read |
billing.subscription.deactivated | The workspace subscription was deactivated. | billing:read |
* is not an event: it is the "everything in the catalog" wildcard, and it covers the ones we add later too. If you would rather hear about a new event before you start receiving it, subscribe to the explicit list.
Three things that save you a debugging session:
channel.run.*is notrun.*. Arun.*is an API Run, with anidyou can query; achannel.run.*is the Met answering a channel message (WhatsApp and the rest) and leaves no row in Runs. If you only listen forrun.completed, channel replies never reach you.app.authorizedandapp.revokedare delivered to the workspace that owns the OAuth app — yours, the developer's — not to the one that authorizes it.- Test deliveries arrive with
type: "ping", which is not in the catalog. Your handler should ignore what it does not recognise instead of failing.
The API returns this same table, always current:
const { data } = await met.webhooks.subscriptions.eventTypes();
// [{ type: 'run.completed', description: '…', scope: 'runs:read' }, …]
Verify the signature
Every delivery carries the header X-Met-Signature: t=<ts>,v1=<hmac>. Never process an event without verifying it — use the SDK helper, which does the verification timing-safe and checks the timestamp (anti-replay):
import Met from '@meteor.ia/sdk';
const met = new Met(process.env.MET_API_KEY!);
app.post('/webhooks/met', (req, res) => {
let event;
try {
event = met.webhooks.constructEvent(req.rawBody, req.headers['x-met-signature'], WHSEC);
} catch {
return res.status(400).send('invalid signature');
}
if (event.type === 'run.completed') { /* ... */ }
res.sendStatus(200); // answer 2xx quickly; Meteor retries otherwise.
});
You need the raw body to verify the signature — configure your framework not to re-serialize the JSON before
constructEvent.
Retries
If your endpoint doesn't answer 2xx, Meteor retries with exponential backoff (1 min → 5 min → 30 min → 2 h → 6 h → 24 h) up to 6 attempts. After that it marks the delivery as failed. The attempt log per endpoint is in your Workbench (Developers section).