Permanently delete a draft campaign

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

Hard-deletes a campaign that is still in draft status. Any other status returns 409 invalid_state; use POST /campaigns/{id}/cancel instead.

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.

Responses

200 Success with empty payload
404 Resource not found within tenant scope
409 Uniqueness/state conflict. Where the cause is actionable, meta.details.code carries it — e.g. invalid_state, concurrent_update, suppression_exists, last_owner_protected. Send-path codes: otp_resend_cooldown (POST /otp/request inside the resend cooldown window of the last still-valid code for the same identifier+purpose), content_moderation_pending (fail-closed block while an AI verdict is unavailable — retryable; POST /transactional/send and /transactional/send-batch also raise it when the moderation check is busy or unreachable), idempotency_key_conflict (same Idempotency-Key replayed with a different request body — permanent, pick a new key), idempotency_in_progress (an earlier request holding the same Idempotency-Key is still executing — transient, retry shortly and the same key will replay its result), otp_reserve_not_applied (the credit reserve for POST /otp/request was declined for a reason other than an empty balance, most often because the ledger already held an entry under the same idempotency key — nothing was charged and nothing was sent; retry), and message_not_scheduled (DELETE /transactional/{message_id} on a message that is not `queued` with a future `send_at` — most commonly, the scheduler already picked it up). Sender profiles add sender_already_exists (POST /senders with a local_part the tenant already uses), sender_in_use (DELETE /senders/{id} while a route or campaign still points at the profile) and default_sender_undeletable (DELETE /senders/{id} on the tenant's default profile — promote another profile with is_default first). POST /domains/{id}/provider adds provider_change_in_progress: another change for this same domain is already committing, most commonly a double-submit — retry once the first one settles.

Example response · 200

{
    "status": "success",
    "http_code": 0,
    "payload": null,
    "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": []
    }
}