Emails are the reusable content assets that blasts and triggered flows send. Each email starts as a draft, accumulates versions as you iterate, and must be approved before a blast or flow can use it.
AI generation is asynchronous. Creating with a prompt or posting a message returns 202 Accepted with a generation_id, and you poll the generation endpoint to track progress. Only one generation can run per email at a time.
POST /v1/emails, POST /v1/emails/{emailId}/messages, and POST /v1/emails/{emailId}/save-code accept an optional Idempotency-Key header. Retrying with the same key replays the stored result instead of creating a duplicate. If a request with the same key is still being processed, the API returns 409 Conflict with error_code: "idempotency_in_progress".
curl --request POST \ --url https://api.tented.ai/v1/emails \ --header "Authorization: Bearer $TENTED_API_KEY" \ --header "Content-Type: application/json" \ --data '{ "name": "Welcome email", "prompt": "A warm welcome email for new Acme Analytics signups with a CTA to book a demo", "subject": "Welcome to Acme" }'
GET /v1/emails/{emailId}/generations/{generationId}
Generation status moves through generating to completed or failed. Completed generations include the version they produced and paths to their content:200 OK
GET /v1/emails/{emailId}/plain-textPUT /v1/emails/{emailId}/plain-textDELETE /v1/emails/{emailId}/plain-text
Every completed generation carries a plain-text alternative derived from its HTML. GET returns it; PUT overrides it with your own text; DELETE reverts to the auto-derived version. All three return the same shape:200 OK
Plain-text body, up to 1 MB. An empty string clears it
PUT and DELETE require a completed generation to exist, otherwise they return 400 Bad Request. stale_after_html_iteration flips to true when the HTML is iterated after an override — a signal to review or revert your custom plain text. The generation-scoped GET /v1/emails/{emailId}/generations/{generationId}/plain-text returns the same shape without the override flags.
Edit instruction against the current HTML, 1-10000 characters
asset_ids
string[]
No
Email asset IDs to make available to the generation
Returns 202 Accepted with a generation_id to poll. Starting a second generation while one is running returns 409 Conflict with error_code: "generation_in_progress".
Creates a new version synchronously and returns 200 OK with the new generation_id and version. Unlike an AI iteration, saving code does not unapprove an approved email. Blocked while a generation is running.
Accepts the same optional fields as create except prompt: name, subject, preview_text, from_name, from_address, reply_to_email. Omit a field to leave it unchanged; pass null to clear nullable fields. Updating preview_text rewrites the preheader in the current HTML in place without creating a new version.
POST /v1/emails/{emailId}/approvePOST /v1/emails/{emailId}/unapprove
Approving marks the email’s current version usable by blasts and flows. It requires subject, from_name, from_address, and reply_to_email to be populated; otherwise the API returns 400 Bad Request with error_code: "email_missing_required_headers".Optionally send {"version": <n>} as an optimistic-concurrency precondition — a mismatch with the current version returns 409 Conflict with error_code: "approval_version_stale".Unapproving is rejected while the email is scheduled in a blast (email_in_scheduled_blast) or used by an active flow (email_in_active_flow).
updated_at, created_at, or name. Only applies when status=any
sort_order
string
desc
asc or desc
limit
number
25
Page size, 1-100
cursor
string
—
next_cursor from a previous response
Responses contain emails and next_cursor. An absent next_cursor means the list is exhausted. List items are the email object minus latest_generation_status and the content_path / plain_text_path fields — lists never inline HTML; fetch it per email via GET /v1/emails/{emailId}/content.
Returns 200 OK with {"email_id": "...", "deleted": true}. Deletion is blocked with 409 Conflict while the email is scheduled in a blast (email_in_scheduled_blast) or used by an active flow (email_in_active_flow).