Launch a campaign now

POST https://emails.routeon.io/api/v1/campaigns/{id}/send

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

{
    "conflict_resolution": "send_anyway"
}

Responses

202 Accepted — launch is async; response includes the campaign in 'sending' state
402 Not enough credits to send, for one of two reasons distinguished by meta.reason: INSUFFICIENT_CREDITS (the tenant's balance is empty) or CREDIT_PROTECTION (the send would cross the tenant's own protected minimum, even though the raw balance would otherwise cover it). meta.details carries code=insufficient_credits or code=credit_protection plus needed, balance and available (integer credits) so the client can state the shortfall.
409 reason=CONFLICT. Two shapes share this status. Same-day conflicts with no conflict_resolution supplied (message=conflicts_detected) carry the aggregated conflict summary in meta.details; retry the request with a chosen conflict_resolution. Every other launch conflict carries meta.details.code instead: invalid_state, tenant_suspended, tenant_needs_moderation, campaign_content_rejected, campaign_needs_content_review, template_changed_since_approval. Branch on the presence of `code`.
422 Body/query/params failed validation. meta.reason=VALIDATION_ERROR, meta.validation_error[] carries {field, message} per failure ('' field = show the message as-is). Always 422 — a request that parsed but broke a rule is never 400. 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), domain_required (neither `domain_id` nor `from` was given), sender_not_configured (no route and no default profile resolves for this send) and sender_not_found (the referenced profile id is unknown to the tenant). 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 check cannot be switched off.

Example response · 202

{
    "status": "success",
    "http_code": 0,
    "payload": {
        "id": "00000000-0000-0000-0000-000000000000",
        "name": "string",
        "type": "blast",
        "status": "draft",
        "domain_id": "00000000-0000-0000-0000-000000000000",
        "template_id": "00000000-0000-0000-0000-000000000000",
        "template_version": 0,
        "audience": {
            "include": [
                {
                    "kind": "list",
                    "id": "00000000-0000-0000-0000-000000000000"
                }
            ],
            "exclude": [
                {
                    "kind": "list",
                    "id": "00000000-0000-0000-0000-000000000000"
                }
            ]
        },
        "schedule_at": "2024-01-01T00:00:00Z",
        "sender_profile_id": "00000000-0000-0000-0000-000000000000",
        "from_name": "string",
        "reply_to": "user@example.com",
        "stats": {
            "sent": 0,
            "delivered": 0,
            "opened": 0,
            "clicked": 0,
            "bounced": 0,
            "complained": 0,
            "failed": 0
        },
        "recipient_count": 0,
        "throttle_per_hour": 0,
        "launched_at": "2024-01-01T00:00:00Z",
        "completed_at": "2024-01-01T00:00:00Z",
        "pause_reason": "string",
        "created_at": "2024-01-01T00:00:00Z",
        "updated_at": "2024-01-01T00:00:00Z"
    },
    "meta": []
}

Example response · 402

{
    "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": "CONFLICT",
        "details": {
            "target_date": "string",
            "audience_size": 0,
            "conflict_count": 0,
            "by_source": [
                {
                    "name": null,
                    "recipients": null
                }
            ],
            "other_sources": 0
        }
    }
}

Example response · 422

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