# Make

> Give a Make scenario its own email address: instant triggers when mail arrives or a thread is handed off, modules to reply, draft and wait for a code.

Give a [Make](https://www.make.com) scenario an email address of its own.
Mail that arrives starts the scenario; the scenario replies, saves drafts for
a person to send, hands threads to people and waits for verification codes.
Nobody's own inbox is connected to Make, and a model in the scenario only
ever reads the one message in front of it.

**The app is not in Make yet.** It is built and run against production every
day, and is waiting to be published in Make. Until it is, each module below
names the one endpoint it calls, so Make's own **HTTP → Make a request**
module with your key in an `Authorization: Bearer` header does the same.

## The connection

Make a key at [Developers → API keys](https://pidgeon.ai/app/developers/keys?utm_source=make),
then in Make: add any Pidgeon module, **Create a connection**, and paste it.
Make tests it against `GET /v1/me` and names the connection after the key.

**One address and one key per scenario, with the least access it needs.**
Whatever the key reaches, the scenario can reach, and so can any model you
give the scenario's output to.

| The scenario                     | Access |
| -------------------------------- | ------ |
| Only watches, searches and reads | read   |
| Saves drafts, hands threads over | draft  |
| Replies or sends                 | send   |
| Creates addresses                | admin  |

A **test key** (`pgn_test_…`) works the same way, and nothing it sends leaves
Pidgeon ([test mode](https://pidgeon.ai/docs/test-mode.md)). Build the scenario on one, deliver
mail to it with `POST /v1/test/inbound`, and switch the connection to a live
key when it works. A test key cannot create addresses.

## The triggers

Both are instant: a scenario runs when the event happens, not on a schedule.
**Create a webhook**, pick the address, and the app creates a webhook on
Pidgeon for that one address. Deleting the webhook in Make deletes it on
Pidgeon. Each one counts against your plan's
[webhook endpoints](https://pidgeon.ai/docs/limits.md) while it exists.

Pidgeon's webhooks carry IDs and metadata, never a body, so each trigger
reads what the delivery names with your connection's key before the scenario
starts. What comes out is what Pidgeon holds, never what the delivery said.

### Watch messages

`GET /v1/messages/{id}`, after a `message.received` delivery. Spam starts it
only with **Include spam** on. The address must keep mail: one without an
inbox stores nothing to read. **Body** chooses the clean text, with anything
hidden or addressed to an AI taken out, or the original.

Every field a router or filter branches on is a plain field, so no model is
needed to decide where mail goes:

```json
{
"event": "message.received",
"event_id": "0044ed22-…",
"message_id": "1a70b25d-…",
"thread_id": "463ec73a-…",
"address": "quotes@yourdomain.com",
"address_id": "9b2f1c3d-…",
"direction": "inbound",
"from": "priya@supplier.de",
"from_name": "Priya Raman",
"to": ["quotes@yourdomain.com"],
"cc": [],
"delivered_to": "quotes@yourdomain.com",
"subject": "Angebot 4471",
"received_at": "2026-10-26T09:14:03Z",
"spam": false,
"firewall_verdict": "clean",
"auth_spf": "pass",
"auth_dkim": "pass",
"auth_dmarc": "pass",
"code": null,
"codes": [],
"links": [],
"text": "Guten Tag, anbei unser Angebot …",
"reply_text": "Guten Tag, anbei unser Angebot …",
"attachments": [],
"test": false
}
```

`firewall_verdict` is `clean`, `suspicious` or `blocked`
([Firewall](https://pidgeon.ai/docs/firewall.md)). Put a filter on it before a model reads `text`.

### Watch thread hand-offs

`GET /v1/threads/{id}`, after a `thread.updated` or `thread.assigned`
delivery. **Direction** is **To a person** (somebody takes the thread, or it
is assigned to somebody), **Back to the agent**, or **Either**. A status
change that moves nobody does not start it.

```json
{
"event": "thread.updated",
"event_id": "7d8e9f0a-…",
"thread_id": "463ec73a-…",
"address": "quotes@yourdomain.com",
"subject": "Angebot 4471",
"from": "priya@supplier.de",
"status": "open",
"owner_type": "human",
"assignee_id": null,
"message_count": 2,
"previous_owner_type": "agent",
"reason": "Asks for a discount above the limit",
"changed_at": "2026-10-26T09:20:11Z"
}
```

Use it to tell a channel or a CRM that a person must act, or to stop a
scenario that would answer a thread somebody now holds.

## The modules

| Module             | Endpoint                       | What it does                                                                              | Access |
| ------------------ | ------------------------------ | ----------------------------------------------------------------------------------------- | ------ |
| Create an address  | `POST /v1/identities`          | An address on your domain or `pidgeon.email`, and who it may write to                     | admin  |
| Send a message     | `POST /v1/messages`            | Writes to people outside your account: a new conversation                                 | send   |
| Reply to a message | `POST /v1/messages/{id}/reply` | Writes to people outside your account: answers in the thread                              | send   |
| Create a draft     | `POST /v1/drafts`              | A draft in the address's Drafts, threaded as a reply                                      | draft  |
| Update a thread    | `PATCH /v1/threads/{id}`       | Hands it to a person, with a reason and an assignee, or back                              | draft  |
| Get a message      | `GET /v1/messages/{id}`        | One message, as the trigger outputs it, clean by default                                  | read   |
| Search messages    | `GET /v1/messages/search`      | Messages by words, address, folder, sender or recipient                                   | read   |
| Get a cursor       | `GET /v1/messages/await`       | Marks now at an address, for Wait for a message                                           | read   |
| Wait for a message | `GET /v1/messages/await`       | The next message to arrive, with its `code`, or `timed_out`                               | read   |
| Make an API call   | any                            | Any endpoint in the [reference](https://pidgeon.ai/docs/reference.md), with the key added | any    |

**A draft waits because nothing sends it.** Give a scenario that drafts a
`draft` key: it cannot send. A person reads the draft and sends it from the
Pidgeon inbox or a [mail client](https://pidgeon.ai/docs/mail-clients.md).

### Waiting for a verification code

Make stops a module after 40 seconds, so **Wait for a message** waits at most
30 and answers as soon as the mail arrives. Put **Get a cursor** before the
step that causes the mail, and its `cursor` into Wait for a message's
**Cursor**:

1. **Get a cursor** for the address.
2. The step that makes the service send mail, such as **HTTP → Make a
   request** to its sign-up form.
3. **Wait for a message**, with the cursor and **Only mail with a code**.
   `code` holds the most likely code.

The cursor means a code that arrives during step 2 is not missed. Without
one, a message from the last 2 minutes counts, so narrow it with **From**.
**From** matches only mail that passed DMARC, because anybody can write a
From line.

A slower sender needs a longer wait: put Wait for a message under a
**Repeater**, pass each run's `cursor` to the next, and stop at the first
bundle with `timed_out` false.

## Templates

Each one is a blueprint: save the file, then in a new scenario **⋯ → Import
blueprint** and choose it. Then pick your connection and address in each module.

- [`draft-and-hand-off.blueprint.json`](https://raw.githubusercontent.com/pidgeon-ai/pidgeon-examples/main/make/draft-and-hand-off.blueprint.json)
  — mail that is clean and not spam gets a drafted acknowledgement, threaded
  as a reply, and the thread is handed to a person with the reason "A draft
  reply is ready to read and send". The key is `draft` on one address.
- [`wait-for-a-code.blueprint.json`](https://raw.githubusercontent.com/pidgeon-ai/pidgeon-examples/main/make/wait-for-a-code.blueprint.json)
  — Get a cursor, then Wait for a message. Insert the step that causes the
  mail between them. The key is `read` on one address.

Mail delivered with `POST /v1/test/inbound` has no SPF, DKIM or DMARC result,
so `auth_dmarc` is empty on a test key. A filter on it stops test mail; take
it out while you test.

Both are also in [pidgeon-ai/pidgeon-examples](https://github.com/pidgeon-ai/pidgeon-examples/tree/main/make).

## Limits

- A trigger watches one address. For several, add a scenario for each, with
  a key for each.
- Each trigger's webhook uses one webhook endpoint against your plan.
- Search messages returns at most 100 messages a page, and stops at **Limit**.
- Wait for a message waits at most 30 seconds a run.
- An address that keeps no mail cannot start Watch messages: there is nothing
  to read.
