Enroll a contact into an automation

POST https://emails.routeon.io/api/v1/automations/{id}/enroll

Identify the contact by contact_id or by email; the contact must already exist in this tenant (404 contact_not_found otherwise). Enrolling a contact that is already enrolled in this automation is not an error — it returns 202 with the existing enrollment_id, so the call is safe to repeat.

Parameters

id string<uuid> required path
Idempotency-Key string header

Replays with the same key return the original result. **Key shape.** A non-empty string of at most 255 characters after trimming surrounding whitespace. An empty or oversized key is rejected with 422 `VALIDATION_ERROR` (`idempotency_key_empty` / `idempotency_key_too_long`) rather than silently running the request unprotected. Leading/trailing whitespace is trimmed, so `" k "` and `"k"` are the same key. **Detecting a replay.** A response served from the idempotency cache rather than by re-executing the operation carries the response header `Idempotency-Replayed: true`; a freshly executed response omits the header entirely. **Replay window.** A record is retained for 24 h by default, after which the same key is free again and the operation re-executes. Only a 2xx outcome is recorded — a request that failed can be retried with the same key. **Conflicts.** Reusing a key with a different request is `409 idempotency_key_conflict`. The request identity is the method, path, query string and body, except on `POST /transactional/send`, where only `domain_id` + `to` (case-insensitive) + `template_id` + `variables` + `send_at` are compared (the parsed instant normalised to ISO-8601, and omitted entirely rather than sent as `null` when the request carried no `send_at`); a replay of that operation does **not** carry `Idempotency-Replayed`. **Concurrency.** A key is claimed before the operation executes, so two requests carrying the same key can never both run. The second one is rejected with `409 idempotency_in_progress` while the first is still in flight. This is a *transient* conflict — distinct from `idempotency_key_conflict` above, which is permanent — so retry after a short delay: once the first request completes, the same key replays its result. A claim is released as soon as the first request fails or returns a non-2xx, and expires after 5 min if the process handling it dies, so a key can never be locked indefinitely. **Rate limiting.** A replay short-circuits before the rate limiter, so it consumes no quota and returns no `X-RateLimit-*` headers. This is deliberate: a client retrying after a timeout must not be penalised for work that was already done. **Scope.** Keys are scoped per tenant.

Request body

{
    "contact_id": "00000000-0000-0000-0000-000000000000"
}

Responses

202 Accepted
404 reason=NOT_FOUND. Two causes: the automation does not exist in this tenant, or the contact named by `contact_id`/`email` does not. Neither carries a meta.details.code — the envelope is a bare `{ reason: NOT_FOUND }`, so the two cases are not distinguishable from the response.
409 reason=CONFLICT. meta.details.code is one of: `campaign_not_automation` (the campaign is not of type automation), `campaign_not_active` (its status is not `sending`), `no_steps` (the automation has no steps to run).
422 Body/query/params failed validation. meta.reason=VALIDATION_ERROR, meta.validation_error[] carries {field, message} per failure ('' field = show the message as-is). A request that parsed but broke a schema rule answers 422. An id in the path, or in the query of an operation that does not take `cursor` and `per_page`, that is not a UUID is rejected with 400 VALIDATION_ERROR before the request is handled. Every query parameter of a list that takes `cursor` and `per_page` (including id filters) is checked before any data is read and answers 422 with that parameter as the field: an invalid, empty or repeated value, or a text value containing the NUL character; unknown parameters are ignored. Sender resolution reports through meta.details.code: invalid_from_address (the `from` string is not `local@domain` or `Name <local@domain>`), from_domain_mismatch (its domain is not a verified domain of the tenant), sender_not_configured (the local part of `from` matches no sender profile of the tenant, or, without `from`, no route and no default profile resolves for this send) and sender_not_found (the referenced profile id is unknown to the tenant). A send with neither `domain_id` nor `from` reports through meta.validation_error (field `domain_id`), not meta.details.code; so do `send_at` failures (field `send_at`, message `send_at_too_far` or `send_at_invalid`). content_moderation_rejected: POST /transactional/send and /transactional/send-batch verify the template's active version before accepting the send, and a rejected verdict fails the request. The same applies to ready-made `subject`/`html`/`text` content. The check cannot be switched off. content_too_large: ready-made content is within the size limits but too big for the moderation check (`details.code`). `html` plus `text` over 4 MB is a plain VALIDATION_ERROR with meta.validation_error field `html`. invalid_variable: a variable name is reserved (`unsubscribe_url`, `this`, `constructor`, `prototype`, or any name starting with `@` or `__`), or `custom` is not an object of strings; the offending name is in `validation_error`. A send that carries both `template_id` and content, or neither, or content with an empty `subject` or with neither `html` nor `text`, is 422 `VALIDATION_ERROR`. new_account_daily_limit_exceeded: POST /campaigns/{id}/send for a new account whose audience exceeds its reduced daily cap (meta.details.limit, needed); it can never fit, so a scheduled launch goes back to draft. throttle_per_hour_too_high: POST /campaigns and PATCH /campaigns/{id} with `throttle_per_hour` above the platform ceiling (meta.validation_error field `throttle_per_hour`, meta.details.max carries the ceiling). PATCH /domains/{id}/warmup accepts only `{state}`; any other property, including `today_cap_override`, or an empty body is 422.

Example response · 202

{
    "status": "success",
    "http_code": 0,
    "payload": {
        "enrollment_id": "00000000-0000-0000-0000-000000000000",
        "status": "active"
    },
    "meta": []
}

Example response · 404

{
    "status": "error",
    "http_code": 0,
    "payload": [],
    "meta": {
        "reason": "VALIDATION_ERROR",
        "validation_error": [
            {
                "field": "string",
                "message": "string"
            }
        ],
        "details": []
    }
}

Example response · 409

{
    "status": "error",
    "http_code": 0,
    "payload": [],
    "meta": {
        "reason": "VALIDATION_ERROR",
        "validation_error": [
            {
                "field": "string",
                "message": "string"
            }
        ],
        "details": []
    }
}

Example response · 422

{
    "status": "error",
    "http_code": 0,
    "payload": [],
    "meta": {
        "reason": "VALIDATION_ERROR",
        "validation_error": [
            {
                "field": "string",
                "message": "string"
            }
        ],
        "details": []
    }
}