Skip to content
Browse the docs

POST /emails

Send an email.

Returns the message id; delivery happens on the queue. Before a domain is verified, from onboarding@agentisend.com delivers only to this account's member sign-in addresses (20 per UTC day, no cc/bcc/attachments/tracking/extra headers). Addresses ending in @simulator.agentisend.com cost nothing and reach nobody.

Request

The same call in curl, Node and Python. A path id in the sample is a placeholder.

curl

curl -sS -X POST https://api.agentisend.com/emails \
  -H "Authorization: Bearer $AGENTISEND_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"from":"billing@yourdomain.com","to":"delivered@simulator.agentisend.com","subject":"Your receipt","text":"Thanks — the details are attached."}'

Node

import { AgentiSend } from 'agentisend';

const client = new AgentiSend(process.env.AGENTISEND_API_KEY);
const result = await client.request({
  method: 'POST',
  path: '/emails',
  body: {
    "from": "billing@yourdomain.com",
    "to": "delivered@simulator.agentisend.com",
    "subject": "Your receipt",
    "text": "Thanks — the details are attached."
  },
});

Python

import json, os, urllib.request

url = 'https://api.agentisend.com/emails'
headers = {'Authorization': 'Bearer ' + os.environ['AGENTISEND_API_KEY']}
payload = json.dumps({"from":"billing@yourdomain.com","to":"delivered@simulator.agentisend.com","subject":"Your receipt","text":"Thanks — the details are attached."}).encode()
headers['Content-Type'] = 'application/json'
request = urllib.request.Request(url, data=payload, headers=headers, method='POST')
print(urllib.request.urlopen(request).read().decode())

Tag: emails. Generated from openapi.json; the anchor post-emails is the id the console's error fix links point at.

Parameters

  • Idempotency-Key header · string · optional — Makes this call safe to retry. Send the same key with the same body and the original response is replayed instead of the work happening twice. Keys are 1-256 characters and are remembered for 7 days. While the first attempt is still running, a second call with that key returns 409 idempotency_in_flight (Resend calls this concurrent_idempotent_requests); the same key with a different body returns 409 idempotency_payload_mismatch (Resend: invalid_idempotent_request). Resend-Idempotency-Key is accepted as the same header.
  • Resend-Idempotency-Key header · string · optional — Alias of Idempotency-Key. Idempotency-Key wins if both are sent.

Request body

FieldTypeRequiredNotes
attachmentsarray<object>noFiles to attach. At most 100, and 40 MB of base64 content together.
attachments[].contentstring | object | array<integer>noBase64 text, a byte array, or a Buffer. Provide content or path.
attachments[].contentIdstringnoSame as content_id. content_id wins if both are sent.
attachments[].contentTypestringnoSame as content_type. content_type wins if both are sent.
attachments[].content_idstringnoReferenced from HTML as cid: this value, for an inline image.
attachments[].content_typestringnoMedia type, such as application/pdf. No parameters.
attachments[].filenamestringnoFile name shown to the recipient.
attachments[].pathstringnohttps URL of a hosted file. Fetched when the send is accepted.
bccstring | array<string>noBlind copy recipients.
ccstring | array<string>noCopy recipients.
expires_atstringnoWhen a held send stops waiting for a person. At most 7 days ahead. Ignored when the send is not held.
fromstringnoSender. A verified domain, or the onboarding address while a domain is still pending. Optional only when the template has a default sender; a from here overrides it.
headersobjectnoExtra headers. Names that this API sets itself are replaced.
htmlstringnoHTML body. Provide html, text, or a template.
replyTostring | array<string>noSame as reply_to. reply_to wins if both are sent.
reply_tostring | array<string>noReply-To addresses. Overrides the template default when a template is sent.
scheduledAtstringnoResend Node SDK name for scheduled_at.
scheduled_atstringnoISO 8601 or a short phrase ("in 1 hour", "tomorrow 9am"). Max 30 days ahead.
subjectstringnoRequired unless template or template_id names a published template that has a subject. Overrides the template subject when both exist.
tagsobject | array<object>noTags as an object of name to value, or a list of {name, value}.
templateobjectnoA published template, instead of html or text.
template.idstringyesPublished template id or alias.
template.variablesobjectnoValues for the template placeholders. A string or a number per name.
template_idstringnoId or alias of the published template to render, instead of html or text.
template_valuesobjectnoValues for the template placeholders.
textstringnoPlain-text body. Provide html, text, or a template.
timezonestringnoTime zone for a scheduled_at phrase such as "tomorrow 9am". An ISO time ignores this. Example: America/Los_Angeles. Omitted phrases are read in UTC.
tostring | array<string>yesRecipients. One address or a list.
topicIdstringnoResend Node SDK name for topic_id.
topic_idstringnoSubscription topic. Recipients who opted out of it are not sent this message.

Responses

  • 201 — Success
  • 400 — The request is malformed or a field failed validation. Codes: validation_error, invalid_idempotency_key, invalid_cursor, plan_not_purchasable, stripe_signature_invalid, support_upload_rejected, appsumo_license_not_confirmed, appsumo_ticket_invalid, sign_in_check_required.
  • 401 — No usable credential was sent. Codes: agent_signup_code_rejected, missing_api_key, session_required, mfa_required, appsumo_signature_invalid.
  • 402 — A budget, quota or plan limit would be exceeded. Codes: overage_cap_reached, daily_limit_reached, agent_budget_exceeded, plan_limit_reached.
  • 403 — The credential may not do this, or the account or key is stopped. Codes: sending_domain_blocked, young_domain_held, domain_sending_disabled, inbound_sender_refused, domain_not_verified, onboarding_recipient_not_a_member, onboarding_sender_unavailable, account_suspended, account_sandboxed, agent_unconfirmed, recipient_ramp, human_action_required, csrf_origin_rejected, charge_not_this_account, invalid_api_key, restricted_api_key, insufficient_role, invite_email_mismatch, domain_scope_violation, dedicated_ip_assigned_by_us, approval_required, staff_review_required, trust_paused, kill_switch_active.
  • 404 — Nothing with this id exists on the account. Codes: billing_customer_missing, not_found, session_expired, support_ticket_not_found.
  • 409 — The request conflicts with the current state. Codes: domain_already_exists, template_name_taken, template_alias_taken, template_in_use, segment_in_use, contact_resubscribe_required, email_still_scheduled, domain_verified_elsewhere, domain_not_claimable, return_path_subdomain_in_use, overage_not_on_plan, term_not_on_sale, subscription_active, portal_change_unavailable, already_in_account, idempotency_in_flight, idempotency_payload_mismatch, approval_expired, support_closed, support_reopen_expired, support_merge_conflict, support_reply_too_soon, appsumo_license_linked, appsumo_license_inactive, lifetime_sold_on_appsumo.
  • 413 — The request body is larger than this operation accepts. Codes: payload_too_large, support_upload_too_large.
  • 415 — The request body is not JSON. Codes: unsupported_media_type.
  • 422 — A field is well formed but was refused. Codes: missing_required_field, invalid_from_address, invalid_parameter, invalid_attachment, invalid_region, tracking_subdomain_unverified, tracking_subdomain_cannot_be_removed, domain_field_immutable, open_tracking_on_transactional, header_replaced, html_clipped_by_gmail, link_domain_listed, domain_blocklisted, mailbox_provider_domain, onboarding_shape_refused, dkim_key_mismatch, domain_check_window_expired, spf_conflict, spf_lookup_limit, review_sandbox_recipient_only, suppressed_recipient, agent_signup_refused, content_refused, recipient_blocklisted, last_owner_required, seat_limit_reached, invite_not_valid, key_budget_exceeds_plan, domain_limit_reached, webhook_endpoint_limit_reached.
  • 429 — A rate, quota or ramp ceiling was reached. Codes: onboarding_daily_cap_reached, trust_throttled, invite_limit_reached, rate_ceiling_exceeded, daily_quota_exceeded, monthly_quota_exceeded, rate_limit_exceeded, support_rate_limited, appsumo_lookup_limited.
  • 500 — Something failed on our side. Codes: internal_server_error.
  • 503 — A dependency is unavailable for a moment. Codes: dns_unreachable, service_unavailable, billing_not_configured, rate_limiter_unavailable, sending_paused_everywhere, support_draft_unavailable.

Response headers

  • ratelimit-limit — Messages this API key may spend in one 60-second window.
  • ratelimit-remaining — Messages left in the current window.
  • ratelimit-reset — Seconds until the current window resets and the budget refills.

201 body

FieldTypeRequiredNotes
idstringyes
simulatedbooleanyes
warningsarray<object>yes
warnings[].codestringyes
warnings[].fixstringyes
warnings[].messagestringyes

Response example

201
{
  "id": "9c8f8f0e-3d1a-4d3f-9a1e-2b7c1a0f5e42",
  "simulated": true,
  "warnings": []
}

Errors

The codes this operation can answer with, and what to do about each. Every one arrives with its code, message, fix and docs_url.

  • account_sandboxed (403) — This account is in the sandbox and may only send to its own verified domains. Fix: Verify the recipient domain with POST /domains + POST /domains/:id/verify, or file POST /trust/appeal for a person to review this account.
  • account_suspended (403) — This account is suspended. Fix: Read the enforcement policy at https://agentisend.com/policy/enforcement, then contact hello@agentisend.com to appeal.
  • agent_budget_exceeded (402) — Key budget for the current period is exhausted. Fix: Wait for the period to reset — get_agent_budget and GET /limits/keys/:id both say when. Raising a budget is a person’s decision, made in the console; a key cannot raise its own.
  • agent_unconfirmed (403) — This key can email only the human who has not confirmed it yet. Fix: POST /agent/verify with the 6-digit code the human received. Until then every recipient has to be that address.
  • approval_required (403) — This action requires human approval before it executes. Fix: Do not send it again: a person approves or rejects it in the console under Agents → Approvals, and approving sends it — the message then appears in GET /emails. action_id in this error names the held send; the key that asked cannot approve itself, and a retry waits on the same approval.
  • content_refused (422) — This email was refused because of what it contains. Fix: The message names the field and what matched in it. Remove or change that part and send again. POST /emails/preflight runs the same check without sending anything.
  • csrf_origin_rejected (403) — This request came from a page on another site, and it changes data. Fix: Call the API with an API key (Authorization: Bearer …) instead of a session cookie, or make the request from the console. Create a key in the console under Settings, API keys.
  • daily_limit_reached (402) — This account has sent its daily allowance of emails; sending resumes at 00:00 UTC. Fix: Wait for 00:00 UTC, or upgrade in Settings → Billing — every paid plan has no daily cap.
  • domain_not_verified (403) — This sending domain is not verified. Fix: If it is pending, publish the required records from GET /domains/:id; checks continue for 72 hours. Test now by sending to an address ending in @simulator.agentisend.com. If it is not registered, POST /domains first. If it is failed, GET /domains/:id names the record to fix, then POST /domains/:id/verify to reopen the window.
  • domain_scope_violation (403) — This API key may only send from its scoped domain. Fix: Send from the scoped domain, or create a key without a domain scope via POST /api-keys.
  • domain_sending_disabled (403) — Sending is turned off for this domain. Fix: Turn it back on with PATCH /domains/:id and {"capabilities": {"sending": "enabled"}}, or send from another domain.
  • idempotency_in_flight (409) — A request with this Idempotency-Key is still in progress. Fix: Wait and retry with the same Idempotency-Key to receive the original response.
  • idempotency_payload_mismatch (409) — Same Idempotency-Key was used with a different payload. Fix: Reuse the exact same body for retries, or send a new Idempotency-Key for a new request.
  • insufficient_role (403) — Your role on this account cannot make this change. Fix: Ask an owner to make the change, or to raise your role with PATCH /team/members/:id.
  • internal_server_error (500, internal_server_error) — Unexpected error. Fix: Retry ONCE after a short pause, with the same Idempotency-Key so the retry cannot double-send. If it fails again, stop retrying and report the x-request-id from the response — that id is what identifies this exact failure in support.
  • invalid_api_key (403, invalid_api_key) — API key is invalid or revoked. Fix: Create a new key with POST /api-keys; deleted keys cannot be restored.
  • invalid_attachment (422, invalid_attachment) — Each attachment needs content (base64) or path. Fix: Provide attachment.content or attachment.path in POST /emails.
  • invalid_from_address (422, invalid_from_address) — The from field is not a valid address. Fix: Use a plain address or "Name <addr@domain>" format in POST /emails.
  • invalid_idempotency_key (400) — Idempotency-Key must be 1-256 characters. Fix: Send a non-empty Idempotency-Key header of at most 256 characters.
  • invalid_parameter (422) — A parameter has an invalid value. Fix: Correct the named parameter and retry.
  • kill_switch_active (403) — This API key has been paused with the kill switch. Fix: Read GET /trust/standing for why it was paused. Only a person signed in to the console can resume it; the paused key cannot resume itself.
  • mfa_required (401) — This session has not completed two-factor authentication. Fix: Finish signing in at https://console.agentisend.com/verify with a code from your authenticator app, or one of your recovery codes. Manage the second factor in the console under Settings, Security.
  • missing_api_key (401, missing_api_key) — Missing API key in authorization header. Fix: Create an API key at https://console.agentisend.com/api-keys and send "Authorization: Bearer as_...". MCP clients can connect with OAuth instead of a key.
  • not_found (404, not_found) — Endpoint or resource does not exist. Fix: List that resource on this account and use an id from the list. For a domain, GET /domains accepts the id or the domain name. A path that is not a route is a typo in the URL.
  • onboarding_daily_cap_reached (429) — This account has used its 20 onboarding emails for today. Fix: Wait until tomorrow (UTC), or verify a domain with POST /domains and send from that domain.
  • onboarding_recipient_not_a_member (403) — The onboarding sender can only mail this account's own members — the addresses they sign in with. Fix: Send to a member sign-in address, or verify a domain with POST /domains and send from that domain.
  • onboarding_sender_unavailable (403) — The onboarding sender is not available. Fix: Send to an address ending in @simulator.agentisend.com, or verify a domain with POST /domains and send from that domain.
  • onboarding_shape_refused (422) — The onboarding sender does not accept cc, bcc, attachments, tracking, or extra headers. Fix: Omit those fields, or verify a domain with POST /domains and send from that domain.
  • overage_cap_reached (402) — This account has used every email in its plan and every extra email its owner allowed this period. Fix: Raise the overage ceiling or move up a plan in Settings → Billing, or wait until the reset date in this error.
  • payload_too_large (413) — Request body is larger than this endpoint accepts. Fix: Send a smaller body. Most endpoints accept 1 MB. Sends (POST /emails, /emails/batch, replies) and template, broadcast and automation edits accept 50 MB, which fits 40 MB of attachments after base64. /mcp accepts 1 MB per JSON-RPC call.
  • plan_limit_reached (402) — This account has used every email included in its plan for the current period. Fix: Upgrade in Settings → Billing, or wait until the reset date in this error.
  • rate_ceiling_exceeded (429) — Per-minute rate ceiling for this key is exhausted. Fix: Wait the seconds below and send the same request again. Raising the ceiling is a person’s decision, made in the console; a key cannot raise its own.
  • rate_limit_exceeded (429, rate_ceiling_exceeded, rate_limit_exceeded) — Too many requests. Fix: Back off and retry honoring the Retry-After header.
  • recipient_blocklisted (422) — This recipient is blocked. Fix: Send to a different address. An address ending in @simulator.agentisend.com is never blocked.
  • recipient_ramp (403) — This key has reached its recipient ramp. Fix: The next recipient slot opens at the time named in this response. POST /emails again after that. Until then, send only to recipients this key has already reached.
  • restricted_api_key (403, restricted_api_key) — This API key is restricted to sending only. Fix: Use a full_access key (POST /api-keys with permission=full_access) for management endpoints.
  • sending_paused_everywhere (503) — Sending is paused for every account while AgentiSend handles an incident. Nothing was sent or charged. Fix: Retry the same request, with the same Idempotency-Key, after the seconds given in Retry-After. Scheduled sends wait and go out once sending resumes.
  • staff_review_required (403) — This message looks like it comes from another company, so a person at AgentiSend reviews it before it is sent. Fix: Nothing else on your account is held. Write to hello@agentisend.com and name the message if it is urgent. You can cancel it instead.
  • suppressed_recipient (422) — Recipient is on the account suppression list. Fix: GET /suppressions says which address and why. A hard bounce you have fixed can be cleared with DELETE /suppressions/:id; an unsubscribe or a spam complaint cannot — that address asked not to be contacted.
  • trust_paused (403) — Sending is paused by the trust system. Fix: Review reasons via GET /trust/standing, then file an appeal via POST /trust/appeal.
  • trust_throttled (429) — This account is being throttled by the trust ladder. Fix: Send volume is temporarily capped because deliverability metrics crossed a published threshold. Check GET /trust/standing for the metric and value, file POST /trust/appeal if this is unexpected.
  • unsupported_media_type (415) — This endpoint does not accept that content type. Fix: Send the body as JSON with `Content-Type: application/json`.
  • validation_error (400, domain_not_verified, invalid_parameter, validation_error) — Error in one or more fields. Fix: Correct the fields listed in the error details and retry the request.
  • young_domain_held (403) — Mail from this domain waits for a review. It was registered less than 30 days ago, or its registration date could not be read. Fix: Mail from your other verified domains still sends. Write to hello@agentisend.com and name this domain.