# Best practices

## Do

- **Use your own ids.** Send `externalRef` for subaccounts and contacts, and upsert contacts with `PUT /v2/contacts/by-external-ref/{externalRef}`.
- **Send an `Idempotency-Key` on every create and send.** Build it from your data so a retry is the same operation. See [Idempotency](/basics/idempotency).
- **Use webhooks instead of polling.** Polling spends your rate limit and arrives late.
- **Branch on `code`.** Show `detail` to people. Codes are stable; messages may improve.
- **Ignore unknown fields and event types in what you receive.** Responses and events gain fields without a new version.
- **Use one key per integration, with the scopes it needs.** Rotate it from the dashboard.
- **Report facts and let triggers choose the message.** Marketing can change wording without a deploy.
- **Check `readiness` after activating an agent**, before you tell your customer it works.
- **Preview campaigns** to see who is skipped.
- **Verify webhook signatures** with the raw request body.
- **Try with `dryRun=true` first** where a route accepts it: nothing is saved or sent.
- **Treat `202` as accepted.** Delivery arrives later.
- **Try with `?dryRun=true` where a route accepts it.** It runs the same checks as the real call and answers `200`. Nothing is saved or sent, and it does not use your `Idempotency-Key`.
- **Send only documented fields.** A request body with an unknown field answers `400 unknown_field`. See [Versioning](/basics/versioning#strict-bodies).

## Avoid

| Avoid | Do this |
| --- | --- |
| Polling messages every second to see if the agent answered | Subscribe to `message.created` |
| A new random `Idempotency-Key` on each retry | Reuse the key on every retry of the same operation |
| Free text on WhatsApp after 24 hours of silence | Send a template |
| Template names and variable positions hard-coded in your backend | Report contact events with named `data`, mapped by a trigger |
| Writing as `team` without taking control | `take-control`, write, `release-control` |
| Parsing ids or storing a slug instead of the id | Treat the full id as an opaque string |
| An API key in a browser or mobile app | Call Lelemon from your server only |
| One key with every scope for all your services | One key per service |
| Deleting and recreating an agent to reset it | Restore a previous version |
| Doing the work before answering a webhook | Answer `2xx` first, then queue the work |

## Limit who can receive messages

The recipient allowlist is a safety net for staging and first launches. While it is on, nothing is sent to anyone outside it.

```bash title="cURL"
curl -X PUT https://api.lelemon.cl/v2/recipient-allowlist \
  -H "Authorization: Bearer $LELEMON_API_KEY" \
  --json '{ "enabled": true, "entries": ["+56912345678", "ana@empresa.cl", "@empresa.cl"] }'
```


Entries are phones in E.164, email addresses or domains written `@domain`. The call replaces the whole list. It needs `recipient-allowlist:write`.
