Send an email
POST https://api.spacepost.dev/emails
Send one email to up to 50 to recipients, with optional cc, bcc, attachments, tags, headers and a schedule. The call returns as soon as the email is accepted and queued; delivery happens asynchronously and is reported by webhooks.
Example
Section titled “Example”import { SpacePost } from "@spacepost/node";
const spacepost = new SpacePost(process.env.SPACEPOST_API_KEY);
const { data, error } = await spacepost.emails.send({ from: "Acme <hello@acme.com>", to: ["user@example.com"], cc: ["manager@example.com"], bcc: ["archive@acme.com"], replyTo: "support@acme.com", subject: "Your invoice", html: "<p>Invoice attached.</p>", text: "Invoice attached.", headers: { "X-Entity-Ref-ID": "inv_1042" }, tags: [{ name: "category", value: "invoice" }],});
if (error) { console.error(error.statusCode, error.name, error.message);} else { console.log("Email id:", data.id);}curl -X POST https://api.spacepost.dev/emails \ -H "Authorization: Bearer $SPACEPOST_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "from": "Acme <hello@acme.com>", "to": ["user@example.com"], "cc": ["manager@example.com"], "bcc": ["archive@acme.com"], "reply_to": "support@acme.com", "subject": "Your invoice", "html": "<p>Invoice attached.</p>", "text": "Invoice attached.", "headers": { "X-Entity-Ref-ID": "inv_1042" }, "tags": [{ "name": "category", "value": "invoice" }] }'import os, requests
r = requests.post( "https://api.spacepost.dev/emails", headers={"Authorization": f"Bearer {os.environ['SPACEPOST_API_KEY']}"}, json={ "from": "Acme <hello@acme.com>", "to": ["user@example.com"], "cc": ["manager@example.com"], "bcc": ["archive@acme.com"], "reply_to": "support@acme.com", "subject": "Your invoice", "html": "<p>Invoice attached.</p>", "text": "Invoice attached.", "headers": {"X-Entity-Ref-ID": "inv_1042"}, "tags": [{"name": "category", "value": "invoice"}], },)print(r.json())payload := map[string]any{ "from": "Acme <hello@acme.com>", "to": []string{"user@example.com"}, "cc": []string{"manager@example.com"}, "reply_to": "support@acme.com", "subject": "Your invoice", "html": "<p>Invoice attached.</p>", "tags": []map[string]string{{"name": "category", "value": "invoice"}},}b, _ := json.Marshal(payload)req, _ := http.NewRequest("POST", "https://api.spacepost.dev/emails", bytes.NewReader(b))req.Header.Set("Authorization", "Bearer "+os.Getenv("SPACEPOST_API_KEY"))req.Header.Set("Content-Type", "application/json")res, err := http.DefaultClient.Do(req)$ch = curl_init('https://api.spacepost.dev/emails');curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('SPACEPOST_API_KEY'), 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'from' => 'Acme <hello@acme.com>', 'to' => ['user@example.com'], 'reply_to' => 'support@acme.com', 'subject' => 'Your invoice', 'html' => '<p>Invoice attached.</p>', 'tags' => [['name' => 'category', 'value' => 'invoice']], ]),]);echo curl_exec($ch);require "net/http"require "json"
uri = URI("https://api.spacepost.dev/emails")req = Net::HTTP::Post.new(uri, "Authorization" => "Bearer #{ENV["SPACEPOST_API_KEY"]}", "Content-Type" => "application/json")req.body = { from: "Acme <hello@acme.com>", to: ["user@example.com"], reply_to: "support@acme.com", subject: "Your invoice", html: "<p>Invoice attached.</p>", tags: [{ name: "category", value: "invoice" }]}.to_jsonres = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }puts res.body{ "id": "0192e1c4-7a3b-7c1e-9d2f-3b5a8e6f4c10" }Request body
Section titled “Request body”fromstringrequiredSender, as email@domain.com or Name <email@domain.com>. The domain (or a parent domain) must be verified. The only exception is onboarding@test.spacepost.dev, see Testing.
tostring | string[]requiredRecipients. Up to 50 addresses.
subjectstringrequired1 to 998 characters.
htmlstringoptionalHTML body. At least one of html or text is required.
textstringoptionalPlain-text body. If you only send html, a text alternative is generated for you, which improves deliverability.
ccstring | string[]optionalUp to 50 addresses.
bccstring | string[]optionalUp to 50 addresses. Never written into message headers.
reply_tostring | string[]optionalUp to 50 addresses. The camelCase alias replyTo is also accepted.
scheduled_atstringoptionalISO 8601 date or natural language such as in 1 hour, at most 30 days ahead. Alias scheduledAt.
headersobjectoptionalCustom headers as string keys and string values.
attachmentsobject[]optionalUp to 50 attachments, each with filename and exactly one of content or path.
tagsobject[]optionalUp to 50 { name, value } pairs. Names and values: 1 to 256 characters of A-Z a-z 0-9 _ -.
tenant_idstringoptionalPlatform mode: send on behalf of a tenant (our id or your external_id). Optional when the from domain belongs to a tenant. Alias tenantId in the Node SDK. See Platform mode.
More: domain verification, Testing, Scheduling, Custom headers, Attachments, Tags, Platform mode.
Addresses in every field may use the Name <email@domain.com> form.
Limits at a glance
Section titled “Limits at a glance”| Limit | Value |
|---|---|
Recipients per field (to, cc, bcc, reply_to) |
50 |
| Subject length | 998 characters |
| Attachments | 50 per email, filename up to 255 characters |
| Tags | 50 per email, name and value 1 to 256 characters of A-Z a-z 0-9 _ - |
| Total message size | 40 MB including attachments and encoding overhead |
| Scheduling horizon | 30 days |
| Emails per batch request | 100 (Batch sending) |
Response
Section titled “Response”Success returns 200 with the new email id (a UUIDv7). Use it with GET /emails/{id} to read the email and its last_event.
What can go wrong
Section titled “What can go wrong”| Status | name |
Meaning |
|---|---|---|
| 401 | missing_api_key |
No Authorization: Bearer header. |
| 403 | invalid_api_key |
Key is unknown, revoked or not a SpacePost key (sp_live_...). |
| 401 | restricted_api_key |
A domain-restricted key was used with a different from domain. |
| 403 | invalid_from_address |
The from domain is not verified for your team. |
| 403 | validation_error |
Using test.spacepost.dev as sender with a recipient that is not a team member or a test address. |
| 403 | sending_paused |
Your team is paused. |
| 422 | validation_error |
A field is missing or invalid. message names the field, for example `to`: Invalid email address. |
| 429 | daily_quota_exceeded |
Daily limit reached: the new-account sandbox, your trust-level cap or a tenant’s daily_limit. |
| 429 | monthly_quota_exceeded |
Free plan monthly limit reached. |
| 429 | rate_limit_exceeded |
More than your requests-per-second limit. See Rate limits. |
See the complete list in Errors.
After the call
Section titled “After the call”The email moves through statuses, exposed as last_event: queued (or scheduled), sent, then delivered, delivery_delayed, bounced, complained, opened or clicked. If every recipient is on your suppression list, nothing is sent and the status becomes suppressed.