# Receive signed events in your backend

Polling the API to find out whether something happened is expensive and always late. With
a subscription, Meteor `POST`s to you when the thing occurs. This recipe builds the whole
endpoint: signature verified, fast response, and processing that holds up under retries.

## Before you start

Your key needs `webhooks:manage`, plus the scope of whatever event you want to hear —
`runs:read` for `run.completed`, `contacts:read` for `contact.created`, and so on. Ask for
an event whose scope you don't hold and it simply won't arrive.

## 1. Subscribe

```ts
import Met from '@meteor.ia/sdk';

const met = new Met(process.env.MET_API_KEY!, {
  workspaceId: Number(process.env.MET_WORKSPACE_ID),
});

const sub = await met.webhooks.subscriptions.create({
  url: 'https://your-server.com/webhooks/met',
  enabled_events: ['run.completed', 'run.failed', 'task.completed'],
  description: 'Results processor',
});

console.log(sub.secret);   // whsec_… — shown ONCE
```

`enabled_events: ['*']` gets you everything. It's worth listing the ones you actually
process: an endpoint that receives seventeen types and acts on three is an endpoint that
falls over because of an event nobody looked at.

The available types come from the API, so there's nothing to guess:

```ts
const types = await met.webhooks.subscriptions.eventTypes();
```

## 2. The endpoint

```ts
import express from 'express';

const app = express();
// RAW body. The signature is computed over the exact bytes Meteor sent: if your
// framework parses and re-serializes the JSON, it stops matching.
app.use('/webhooks/met', express.raw({ type: 'application/json' }));

app.post('/webhooks/met', async (req, res) => {
  let event;
  try {
    event = met.webhooks.constructEvent(
      req.body,
      req.headers['x-met-signature'] as string,
      process.env.MET_WEBHOOK_SECRET!,
    );
  } catch {
    // Bad signature or stale timestamp. Don't retry: it won't get better.
    return res.status(400).send('invalid signature');
  }

  // 2xx first, work second: Meteor treats a delivery that doesn't answer as
  // failed, and your processing can take longer than its timeout.
  res.sendStatus(200);
  await enqueue(event);
});

app.listen(3000);
```

`constructEvent` verifies the signature in constant time and checks the timestamp in the
`X-Met-Signature: t=<ts>,v1=<hmac>` header, which is what stops someone from replaying an
old, legitimate delivery at you.

## 3. Surviving retries

If your endpoint doesn't answer `2xx`, Meteor retries with growing backoff — 1 min, 5 min,
30 min, 2 h, 6 h, 24 h — up to six times. That means **the same event can reach you more
than once**, and it can also arrive late and out of order.

Record the event id before you act:

```ts
async function enqueue(event) {
  const isNew = await yourDb.insertIfMissing('met_events', { id: event.id });
  if (!isNew) return;              // already processed: this delivery is a retry
  await process(event);
}
```

The rule that saves you a bad afternoon: **arrival order is not the order things
happened.** If your logic depends on sequence, order by the event's `created` — a
seconds-epoch — and not by when you received it.

Every delivery has this shape:

```json
{
  "id": "evt_9f2c…",
  "object": "event",
  "type": "run.completed",
  "created": 1786886400,
  "livemode": true,
  "data": { }
}
```

## 4. Testing it before anything real happens

```ts
await met.webhooks.subscriptions.test(sub.id);   // a test delivery, signed the same way
```

And against real traffic, without deploying:

```bash
met listen --forward http://localhost:3000/webhooks/met --types run.completed,run.failed
```

If a delivery failed and you've already fixed the cause, you can retry it by hand from the
delivery log, in your dashboard under **Developers → Activity**.

## Next

- The full event catalog and the retry detail: [Webhooks](webhooks.html).
- If what you want is to react within the same second rather than on the next delivery:
  [Live events](events.html).
