# An agent that answers WhatsApp

With autopilot on, Meteor already answers WhatsApp messages by itself. This recipe is for
the other case: when you want **your own logic in the middle** — checking your inventory
before replying, escalating based on the customer, writing to your CRM, deciding when the
Met goes quiet and a person takes over.

## Before you start

Connect the WhatsApp channel to your workspace from the dashboard (**Channels →
WhatsApp**). The API doesn't connect channels: that happens once and needs Meta's
approval.

Your key needs five scopes, one per thing the recipe does:

| scope | what for |
|---|---|
| `webhooks:manage` | registering the endpoint that receives events |
| `conversations:read` | receiving `contact.message.received` |
| `runs:execute` | running the Met |
| `channels:send` | replying to the contact |
| `handoff:manage` | turning autopilot off and on |

## 1. Subscribe to incoming messages

```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: ['contact.message.received'],
});

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

Store the `secret` the moment you get it: you need it to verify every delivery and it is
never shown again.

## 2. Receive the message and reply

The event arrives with this body:

```json
{
  "contact_id": 4821,
  "message_id": 99312,
  "channel_id": 12,
  "channel_type": "whatsapp",
  "content": "Do you still have the blue bike?",
  "attachments": []
}
```

```ts
import express from 'express';

const app = express();
// The RAW body is mandatory: if your framework re-serializes the JSON before you
// verify, the signature stops matching even though the content is identical.
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 {
    return res.status(400).send('invalid signature');
  }

  // Answer NOW. Meteor retries when it doesn't see a 2xx, and a Met can take
  // several seconds: replying after thinking gets you duplicate messages.
  res.sendStatus(200);

  if (event.type !== 'contact.message.received') return;

  const { contact_id, content } = event.data;
  const run = await met.runs.create({ input: content });
  await met.contacts.sendMessage(contact_id, run.output);
});

app.listen(3000);
```

A run created through the API isn't bound to the contact: it gets the text you pass and
nothing else. If you want the Met to answer knowing who it's talking to, build the input
yourself — with `met.contacts.retrieve(contact_id)` and `met.contacts.messages(contact_id)`,
for instance — or use `conversation_id` so the conversation keeps memory across runs.

## 3. Hand it to a person when it's needed

A Met that doesn't know something has to be able to let go of the conversation. With
autopilot off, messages keep arriving in your inbox but the Met stops replying to that
contact until you turn it back on.

```ts
if (/talk to (someone|a person|an agent)/i.test(content)) {
  await met.contacts.setAutopilot(contact_id, false);
  await met.contacts.sendMessage(contact_id, 'Connecting you with someone on the team 👋');
  await met.contacts.createNote(contact_id, 'Asked for a human.');
  return;
}
```

To give control back to the Met: `met.contacts.setAutopilot(contact_id, true)`.

## The 24-hour window

This isn't a Meteor limit and there's no way around it: **WhatsApp only lets you write
freely for 24 hours after the person's last message.** Past that window, the only thing
that gets through is a Meta-approved template:

```ts
await met.contacts.sendTemplate(contact_id, {
  name: 'appointment_reminder',
  language: 'en',
  namespace: process.env.WA_TEMPLATE_NAMESPACE!,   // required
  params: { '1': 'Ana', '2': 'Tuesday at 3' },
});
```

If your flow replies late — a queue, an overnight retry — that window is the first thing
to check, before the key or the scopes.

## Trying it without deploying anything

The CLI forwards your workspace's real events to your machine, so you can write the
handler against actual traffic before you have a server:

```bash
met listen --forward http://localhost:3000/webhooks/met --types contact.message.received
```

## Next

- Every event's schema, the retries and the delivery log: [Webhooks](webhooks.html).
- Replying live, word by word, instead of waiting for the whole run:
  [Show a run live in your UI](recipe-streaming.html).
- Letting the Met read your own data while it answers:
  [Data: collections and items](collections-and-items.html).
