On this page
Guides / Inbound webhooks

Inbound webhooks

Register endpoints that trigger tasks or emit events, with HMAC signing.

Updated View .md

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.

Change an endpoint's events

To add or remove events, or change the URL or description, use PATCH /webhook-endpoints/{id}. The signing secret doesn't change: your verification keeps working as is, with nothing to redeploy. enabled_events replaces the whole list, so to add an event send the current list plus the new one:

const [ep] = await met.webhooks.subscriptions.list();
await met.webhooks.subscriptions.update(ep.id, {
  enabled_events: [...ep.enabled_events, 'contact.message.failed'],
});

If you subscribed with *, there's nothing to do: new catalog events reach you on their own.

Event catalog

There are 30. 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.

EventWhen it firesScope
run.completedA Run finished successfully.runs:read
run.failedA Run failed.runs:read
channel.run.completedThe Met answered a channel message.runs:read
channel.run.failedThe Met failed to answer a channel message.runs:read
contact.createdA contact was created.contacts:read
contact.updatedA contact was updated.contacts:read
contact.message.receivedA message from a contact came in through a channel (WhatsApp, etc.).conversations:read
conversation.handoffA conversation moved to a human operator.conversations:read
task.completedAn agentic task finished.tasks:read
billing.thresholdAn API key's Energy spend crossed a threshold of its budget (50/80/100%).billing:read
app.authorizedA workspace authorized your OAuth app (new grant).integrations:read
app.revokedA workspace revoked your OAuth app's access (grant revoked).integrations:read
snapshot.publishedOne of your templates was approved and published to the marketplace.snapshots:read
snapshot.install.completedA template install finished.snapshots:read
snapshot.install.failedA template install failed.snapshots:read
conversion.sentA conversion was emitted successfully to Meta (CAPI).conversions:read
conversion.discardedA proposed conversion was discarded by the gate (the reason is included).conversions:read
automation.run.failedAn automation failed to run.automations:read
contact.message.sentA message went out to a contact, whether the Met or a person wrote it.conversations:read
contact.message.failedA message to a contact couldn't be delivered. Carries the provider's error code.conversations:read
contact.deletedA contact was deleted.contacts:read
broadcast.completedA broadcast finished sending (includes how many went out and how many failed).channels:read
channel.template.approvedMeta approved a WhatsApp template.channels:read
channel.template.rejectedMeta rejected a WhatsApp template (includes the reason).channels:read
channel.template.status_updatedA WhatsApp template changed status for another reason (paused, disabled, under appeal…): carries Meta's status.channels:read
call.missedA call went unanswered.conversations:read
billing.energy.lowThe Energy balance crossed below the low threshold. It fires on the crossing, not on every execution.billing:read
billing.energy.depletedThe Energy balance ran out.billing:read
billing.energy.rechargedAn Energy top-up came in (purchase, auto-recharge, or a partner credit).billing:read
billing.subscription.deactivatedThe 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.

Five things that save you a debugging session:

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 within 15 seconds, Meteor retries with growing waits: the first attempt, then after 1 min → 5 min → 30 min → 2 h → 6 h, 6 attempts in total (about 8.6 hours from the first). After that it marks the delivery as failed; you can send it again with POST /webhook-endpoints/{id}/deliveries/{deliveryId}/retry or POST /events/{id}/replay. Every retry carries the same event id, also in the X-Met-Event-Id header: use it so you never process the same thing twice. Deliveries go out in parallel, so ordering is not guaranteed: sort by created. The attempt log per endpoint is in your Workbench (Developers section).