> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lekalao.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Test keys

> Try an integration end to end, webhooks included, without sending a single e-mail.

A test key is an API token that sends nothing. A transactional e-mail sent with it is checked, rendered and logged exactly like a real one, then marked `sent` without any provider hearing of it, and counted against nothing. Sent to one of the [sink addresses](#the-sink-addresses), it also gets the answer a provider would have given, with the webhooks that go with it.

## Create a test key

**Settings → API tokens → New token**, then switch on **Test key** before **Create**. The list shows it with a **Test** badge. A key is a test key from birth and stays one: to go live, create a live key and swap it in your configuration.

Your application can ask which one it holds:

```bash theme={null}
curl https://api.lekalao.com/v1/me \
  -H "Authorization: Bearer $LEKALAO_TEST_TOKEN" \
  -H "Accept: application/json"
```

```json theme={null}
{ "data": { "team": { "id": "01a0bb7d-…", "name": "Acme" }, "user": { "…": "…" }, "abilities": ["read", "write"], "sandbox": true } }
```

## What a test key changes

| Call | With a test key |
| - | - |
| `POST /v1/transactional-mails/send` | Validated, checked against the suppression list and the sender policy, rendered and logged, then `sent` with `"sandbox": true`. No provider, no allowance used. The `transactional.*` webhooks fire. |
| `POST /v1/transactional-mails/{mail}/resend` | The same. A test e-mail sent again stays a test e-mail, whatever the key. |
| `POST /v1/campaigns/{campaign}/test` | Every check is made and the campaign is rendered, then nothing leaves: `"sandbox": true`, nothing counted. |
| `POST /v1/campaigns/{campaign}/send`, `POST /v1/automations/{automation}/trigger`, `POST /v1/subscribers/{subscriber}/resend-confirmation` | Refused, `403` [`sandbox_cannot_send`](https://docs.lekalao.com/errors#sandbox_cannot_send): they exist to write to real people, which cannot be simulated in part. |
| The [SMTP relay](https://docs.lekalao.com/developers/smtp-relay.md), with the test key as its password | Every message is logged as a test e-mail, and none leaves. |
| Everything else | As a live key. |

> [!WARNING]
>
> A test key reads and writes the team's **real** lists, subscribers,
> templates and webhooks. Subscribing someone with it may send them a real
> confirmation e-mail or start an automation. For an integration that
> creates contacts while you try it, use a team of its own: a test team,
> with its own keys.

In the log, **Transactional**, a test e-mail carries a **Test** badge. `GET /v1/transactional-mails?sandbox=true` lists only those, `?sandbox=false` only the real ones.

## The sink addresses

Write to these addresses to get a provider's answer without one. The part after `+` is free: `bounced+signup-form@sink.lekalao.dev` is a bounce too, so that several tests can be told apart.

| Address | What happens, a moment after `transactional.sent` |
| - | - |
| `delivered@sink.lekalao.dev` | Delivered: `delivered_at` is set, `transactional.delivered` fires. |
| `bounced@sink.lekalao.dev` | A hard bounce: `bounced_at` and `failure_reason` are set, `transactional.bounced` and `mail.bounced` fire. |
| `complained@sink.lekalao.dev` | Delivered, then reported as spam: `transactional.delivered`, `transactional.complained` and `mail.complaint`. |
| `opened@sink.lekalao.dev` | Delivered, then opened: `transactional.opened`, `opens` is 1. |
| `clicked@sink.lekalao.dev` | Delivered, opened, and its first tracked link clicked: `transactional.clicked` with the `url`. |
| `failed@sink.lekalao.dev` | The provider refuses it: the call answers `502` [`provider_error`](https://docs.lekalao.com/errors#provider_error), and `transactional.failed` fires. |

The webhooks are the real ones, signed, retried and logged like any other, with `"sandbox": true` in their `data`. A simulated bounce or complaint puts **nothing** on the suppression list: the same test runs again tomorrow.

Any other address is logged and marked `sent`, and nothing more happens to it.

> [!NOTE]
>
> An e-mail written **only** to sink addresses never reaches a provider,
> whatever the key: it would bounce off a domain that receives nothing, at
> the expense of your sender's name. Mixed with real addresses, with a live
> key, it is sent like any other.

## Try it from a terminal

No collection to import, no account elsewhere: `curl` and a test key are enough.

**1. A test key**

Create one as above, then:

```bash theme={null}
export LEKALAO_TEST_TOKEN="12|kJ3v8mQ2xP9…"
```

**2. An endpoint for the webhooks**

Your application's, reachable from the internet; on your machine, through a tunnel such as ngrok or Cloudflare Tunnel. Register it for the transactional events, and keep the `secret` of the answer:

```bash theme={null}
curl -X POST https://api.lekalao.com/v1/webhooks \
  -H "Authorization: Bearer $LEKALAO_TEST_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "url": "https://your-tunnel.example/webhooks/lekalao", "events": ["transactional.sent", "transactional.delivered", "transactional.bounced", "transactional.failed"] }'
```

Skip this step to see the calls only in **Settings → Outgoing webhooks**: the log shows what would have been sent, and its answer.

**3. An e-mail that bounces**

```bash theme={null}
curl -X POST https://api.lekalao.com/v1/transactional-mails/send \
  -H "Authorization: Bearer $LEKALAO_TEST_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "to": ["bounced@sink.lekalao.dev"], "subject": "Your order", "html": "<p>Thanks!</p>" }'
```

The answer is `201`, `"status": "sent"`, `"sandbox": true`. Keep its `id`.

**4. What your application received**

`transactional.sent`, then `transactional.bounced` with `"type": "hard_bounce"` and the address. Check each signature as [Webhooks](https://docs.lekalao.com/developers/webhooks.md#check-the-signature) shows: a handler that refuses a forged call is part of the test.

Read the e-mail again: `bounced_at` is set.

```bash theme={null}
curl https://api.lekalao.com/v1/transactional-mails/$MAIL_ID \
  -H "Authorization: Bearer $LEKALAO_TEST_TOKEN" \
  -H "Accept: application/json"
```

**5. The other answers**

The same call to `delivered@`, `complained@`, `opened@`, `clicked@` and `failed@sink.lekalao.dev`: each of your handlers runs once. Then swap the test key for a live one.

To generate a client rather than write the calls, take the API's own description: `GET /v1/openapi.json` (`?lang=fr` for the French one). It is the same document as the [API reference](https://docs.lekalao.com/api-reference/introduction.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.