# Test mode

> Test-mode keys for Pidgeon: the whole pipeline — threading, events, webhooks, the Firewall — with no mail sent, nothing counted, and simulated inbound messages for testing replies.

A **test key** (`pgn_test_…`) does everything a live key does, and none of
its mail leaves Pidgeon. Use one in CI, in a staging environment, or
anywhere an agent is being built and should not be writing to real people.

## What runs, and what does not

Everything runs: validation, the send checks (who may send as which address,
suppression, the address's send policy), threading, the stored message,
events and their webhooks, receipts, `Idempotency-Key`, and the Firewall on
mail that arrives.

What does not:

- **Nothing is handed to the mail provider.** Not for a send, a reply, a
  forward, a draft sent later, a scheduled send, or a calendar invitation.
  Whatever the recipient, whichever route or retry.
- **Nothing counts.** No daily sending allowance, no usage, no billing. The
  five-minute and per-minute limits still apply, at your plan's numbers, but
  in a bucket of their own. A test suite cannot use up what your production
  server needs.
- **No person is notified** by email or push about test mail.

Every message written in test mode has `"test": true` — on the message, on
the answer to a send, and on `message.sent` and `message.received`.

## Making one

Pass `test: true` when you mint the key. A test key's own children are
always test keys.

```bash
curl https://pidgeon.ai/api/v1/api-keys \
-H "Authorization: Bearer $PIDGEON_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "CI", "identity_ids": ["identity-id"], "test": true }'
```

```ts
const { data: key } = await keysCreate({
body: { name: 'CI', identity_ids: [identityId], test: true },
throwOnError: true,
});

// pgn_test_…
process.env.PIDGEON_API_KEY = key.data.token;
```

```python
from pidgeon.api.keys import keys_create
from pidgeon.models import KeysCreateBody

key = keys_create.sync(
  client=client,
  body=KeysCreateBody(name="CI", identity_ids=[identity_id], test=True),
)
```

A test key works on **the addresses it was given**. It cannot create or
delete addresses, domains or pods, or issue IMAP and SMTP passwords: each of
those counts against your plan, reaches the provider, or opens a way to send
that test mode does not cover. Make the address with a live key, then mint a
test key for it.

## Where test mail goes

A test send to one of **your own account's addresses** is delivered there,
marked test, so two of your agents can talk to each other in CI. A test
send to anybody else, including an address in another Pidgeon account, is
accepted, stored as sent, and goes nowhere.

A reply to a test message is a test message too, whoever sends it. If a
person answers a simulated customer from the dashboard, that reply also stays
inside.

## Playing the other side

Nobody outside will ever answer a test send, so `POST /v1/test/inbound`
plays the other side. It delivers a message to one of the test key's
addresses through the real inbound pipeline, exactly as an email from
outside would arrive. Pass `in_reply_to` with a message id to have it arrive
as a reply in that thread.

```bash
curl https://pidgeon.ai/api/v1/test/inbound \
-H "Authorization: Bearer $PIDGEON_TEST_KEY" \
-H "Content-Type: application/json" \
-d '{
  "to": "support@yourapp.pidgeon.email",
  "from": "customer@example.com",
  "subject": "Re: Your order",
  "text": "It never arrived.",
  "in_reply_to": "message-id"
}'
```

```ts
const { data } = await testInbound({
body: {
  to: 'support@yourapp.pidgeon.email',
  from: 'customer@example.com',
  subject: 'Re: Your order',
  text: 'It never arrived.',
  in_reply_to: sentMessageId,
},
throwOnError: true,
});

// Your webhook gets message.received; your agent answers it.
console.log(data.data.message_id, data.data.thread_id);
```

```python
from pidgeon.api.test import test_inbound
from pidgeon.models import TestInboundBody

delivered = test_inbound.sync(
  client=client,
  body=TestInboundBody(
      to="support@yourapp.pidgeon.email",
      from_="customer@example.com",
      text="It never arrived.",
      in_reply_to=sent_message_id,
  ),
)
```

```json
{
"data": {
  "status": "delivered",
  "message_id": "…",
  "thread_id": "…",
  "identity_id": "…",
  "spam": false
}
}
```

- **Test keys only.** A live key gets `403`: it would put a made-up sender
  into a real inbox.
- **An address the key reaches at `send`.** Anything else is `404`, the
  same as an address that does not exist.
- The sender's rules, the spam filter and the Firewall all judge it. A
  sender rule that rejects `from` rejects it here, with `status: "rejected"`.
- Up to 1,000 an hour per account. It takes an `Idempotency-Key`, so a CI
  step that retries does not deliver twice.
