Documentation

Everything you need to send email with Sendscape

Getting started

Three steps from zero to your first delivered email.

1
Verify a domain
Add your domain under Domains and create the three CNAME records at your DNS provider. You can only send from verified domains.
2
Create a credential
Create an API key for the HTTP API, or an SMTP credential for a drop-in relay setup. Both are shown once - store them safely.
3
Send your first email
Copy a snippet from the HTTP API or SMTP section below and fire a test send. It shows up in your send log immediately.

Verify a domain

Add a domain and you get three CNAME records that set up email signing (DKIM). Create them at your DNS provider, and the dashboard checks automatically every 15 seconds until they resolve. Verification usually completes within minutes, but DNS propagation can take up to 48 hours.

Once verified, you can send from any address at that domain - and at its subdomains. Verifying example.com also lets you send from news.example.com. The Free plan includes 1 sending domain; paid plans include more (see Quotas and plans).

Your records and live verification status are on the Domains page.

Send via HTTP API

Send by POSTing JSON to https://api.sendscape.ai/v1/emails with your API key as a bearer token: Authorization: Bearer YOUR_API_KEY. Keys start with ss_ and are created on the API Keys page.

Request fields

FieldTypeRequiredNotes
fromstringYesSender address, either "Acme <hello@yourdomain.com>" or a bare address. The domain must be verified on your account; a verified parent domain also covers its subdomains. onboarding@sendscape.ai is accepted only when every recipient is a sandbox address.
tostring | arrayYesA single recipient, or an array of addresses. Everyone listed gets the same email and can see each other in the To field - to send each person a private copy, make one request per recipient. to + cc + bcc together may not exceed 50, and one request counts once against your quota regardless of recipient count.
ccstring | arrayNoCarbon-copy recipients. Same cap and suppression rules as to.
bccstring | arrayNoBlind-copy recipients - never visible to the others. Same cap and suppression rules.
subjectstringYesMax 255 characters.
htmlstringNo*HTML body. At least one of html or text is required.
textstringNo*Plain-text body. Sending it alongside html helps deliverability.
reply_tostring | arrayNoOne or more Reply-To addresses (max 10).
headersobjectNoUp to 10 custom headers, e.g. {"X-Entity-Ref-ID": "order-42"}. Names use letters, digits and hyphens; values are one line. Headers the mail system owns (From, To, Cc, Bcc, Subject, Date, Message-ID, Content-*, List-Unsubscribe-Post, X-SES-*) are refused with invalid_header. List-Unsubscribe is allowed.
attachmentsarrayNoUp to 10 files as {filename, content, content_type?, content_id?}. content is base64; 10 MB decoded in total. Executables and scripts are refused. Set content_id to embed an image inline via cid:. Not accepted on the batch endpoint.
tagsarrayNoUp to 10 {name, value} pairs (letters, digits, - and _, max 256 chars each). Returned on the email object and filterable with GET /v1/emails?tag=name:value.

Example request

curl -X POST https://api.sendscape.ai/v1/emails \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "hello@yourdomain.com",
    "to": ["recipient@example.com"],
    "subject": "Hello from Sendscape",
    "html": "<p>It works!</p>"
  }'

Success response

Accepted sends return 202. The id is the send-log entry you can find under Email Metrics.

HTTP/1.1 202 Accepted

{
  "id": 42,
  "message_id": "0107019064ab1234-56c78d90-...",
  "status": "accepted"
}

Errors

Errors use a consistent envelope: an error machine code plus a human-readable message.

StatusCodeMeaning
401missing_api_keyNo Authorization header, or the token does not start with ss_.
401invalid_api_keyThe key was not found or has been revoked.
401expired_api_keyThe key passed its expiry date. Create a new one.
403insufficient_scopeA sending-only key was used on a read endpoint. Use a full-access key.
403key_domain_mismatchThe key is bound to a different sending domain than the from address.
422(validation)A field failed validation. Standard envelope with "message" and per-field "errors".
422missing_bodyNeither html nor text was provided.
422domain_not_verifiedThe from domain is not verified on your account.
422recipient_suppressedA recipient is on your suppression list.
422too_many_recipientsMore than 50 recipients (to + cc + bcc) in one message.
422invalid_headerA custom header uses a reserved name, a bad character, or a list instead of an object.
422invalid_tagA tag name or value is outside letters, digits, - and _.
422invalid_attachmentAn attachment has no filename, is not valid base64, or has a bad content type.
422attachments_too_largeAttachments exceed 10 MB decoded in total.
422attachment_type_not_allowedExecutable or script attachments are refused.
422attachments_not_allowed_in_batchAttachments are only accepted on POST /v1/emails.
422invalid_batchThe batch body is not a JSON array of 1 to 100 messages.
422batch_validation_failedOne or more batch messages failed validation. Nothing was sent; see errors by index.
422invalid_cursorThe list cursor was not one this API issued.
422idempotency_key_reusedThe Idempotency-Key was already used with a different request body.
422idempotency_key_invalidThe Idempotency-Key is longer than 256 characters.
422sandbox_mixed_recipientsSandbox and real recipients cannot be mixed in one message.
422invalid_sandbox_addressOnly delivered@, bounced@, complained@ and suppressed@sandbox.sendscape.ai exist.
403account_pausedYour account is paused and cannot send.
403tenant_pausedThe mail provider paused this account for reputation. Contact support.
404not_foundNo email with that id on your account.
409idempotency_in_progressA request with the same Idempotency-Key is still being processed. Retry shortly.
413payload_too_largeThe request body is over the size limit (about 15 MB).
429quota_exceededYour daily or monthly sending quota has been reached. Retry after it resets.
429rate_limitedToo many requests in a minute (600 sends, 20 batches, 300 reads). Retry after the Retry-After header.
502ses_errorThe upstream mail provider rejected the message. Your quota is refunded - safe to retry.
HTTP/1.1 422 Unprocessable Content

{
  "error": "domain_not_verified",
  "message": "The sending domain 'example.com' is not verified for this account."
}

Advanced fields

cc, bcc, a reply-to list, custom headers, tags, an inline attachment and an Idempotency-Key, all in one request. Attachments are capped at 10 MB decoded per message.

curl -X POST https://api.sendscape.ai/v1/emails \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-42-receipt" \
  -d '{
    "from": "Acme <billing@yourdomain.com>",
    "to": ["customer@example.com"],
    "cc": ["accounts@example.com"],
    "bcc": "archive@yourdomain.com",
    "reply_to": ["support@yourdomain.com"],
    "subject": "Receipt for order 42",
    "html": "<p>Thanks! <img src=\"cid:logo\"></p>",
    "headers": { "X-Entity-Ref-ID": "order-42" },
    "tags": [{ "name": "category", "value": "receipt" }],
    "attachments": [{ "filename": "logo.png", "content": "iVBORw0KGgo...", "content_type": "image/png", "content_id": "logo" }]
  }'

Idempotency

Add an Idempotency-Key header (any string, unique per logical send) and a retried request returns the original outcome instead of sending twice. Remembered for 24 hours per API key; quota and pause errors are not remembered, so the same key can be retried later.

POST /v1/emails
Idempotency-Key: order-42-receipt

# first call -> 202 {"id": 42, ...}
# same key + same body within 24 h -> the same 202 body, plus:
Idempotency-Replayed: true
# same key + a different body -> 422 idempotency_key_reused

Retrieve and list

GET /v1/emails/{id} returns the email object; GET /v1/emails lists yours newest first with a cursor. Both need a full-access key.

HTTP/1.1 200 OK

{
  "id": 42,
  "message_id": "0107019064ab1234-56c78d90-...",
  "from": "hello@yourdomain.com",
  "to": ["user@example.com"],
  "cc": [],
  "bcc": [],
  "reply_to": ["support@yourdomain.com"],
  "subject": "Your receipt",
  "tags": [{ "name": "category", "value": "receipt" }],
  "status": "delivered",
  "error": null,
  "channel": "api",
  "created_at": "2026-09-07T10:15:02+08:00",
  "last_event_at": "2026-09-07T10:15:09+08:00",
  "bounce_type": null,
  "opened_at": "2026-09-07T10:21:40+08:00",
  "clicked_at": null,
  "open_count": 1,
  "click_count": 0
}
ParameterMeaning
limitPage size, 1-100 (default 20).
cursorThe next_cursor from the previous page. Omit for the first page.
statusaccepted (includes delayed), delayed, delivered, bounced, complained, failed (includes rejected) or rejected.
tagname or name:value - only emails carrying that tag.
toExact recipient address (the first To).
fromExact sender address.
since / untilDate (YYYY-MM-DD, whole day) or ISO 8601 timestamp bounds.

Send a batch

POST /v1/emails/batch takes an array of up to 100 messages (no attachments). Every message is validated before any is sent; a valid batch is sent in order with one result per message.

POST /v1/emails/batch
Idempotency-Key: nightly-2026-09-07

[
  { "from": "hello@yourdomain.com", "to": ["a@example.com"], "subject": "Hi A", "text": "..." },
  { "from": "hello@yourdomain.com", "to": ["b@example.com"], "subject": "Hi B", "text": "..." }
]

Sandbox addresses

Exercise the whole pipeline - log rows, statuses, webhooks - without real mail or quota. Works from any verified domain, or from onboarding@sendscape.ai before you have one.

RecipientWhat happens
delivered@sandbox.sendscape.aiAccepted, then marked delivered. Webhooks: email.sent, email.delivered.
bounced@sandbox.sendscape.aiAccepted, then a simulated permanent bounce. Nothing is added to your suppression list.
complained@sandbox.sendscape.aiAccepted, then a simulated spam complaint. Nothing is suppressed.
suppressed@sandbox.sendscape.aiRefused with 422 recipient_suppressed, so your client can exercise that branch.

API key permissions

A key is full-access by default. A sending-only key can call the send endpoints but not read your log, and a key can be bound to one sending domain.

PermissionCan sendCan read emailsNotes
full_access (default)YesYesEvery key created before permissions existed.
sending_accessYesNoRead endpoints answer 403 insufficient_scope. Embed this one in apps that only send.

Send via SMTP

Anything that speaks SMTP - Laravel, WordPress, legacy apps, IoT devices - can send through Sendscape without code changes. Create a credential on the SMTP Credentials page and plug in these settings:

SettingValue
Hostsmtp.sendscape.ai
Port587
EncryptionSTARTTLS
Usernamess_... (shown on the credentials page)
PasswordShown once when you create the credential

SMTP goes through the same pipeline as the HTTP API: the from domain must be verified, suppressed recipients are rejected, and sends count against the same quota.

MAIL_MAILER=smtp
MAIL_HOST=smtp.sendscape.ai
MAIL_PORT=587
MAIL_USERNAME=YOUR_SMTP_USERNAME
MAIL_PASSWORD=••••••••   # shown once when you create the credential
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=hello@yourdomain.com

Connect n8n

Use Sendscape in an n8n workflow with built-in nodes - no custom node required. Either the HTTP Request node with an API key, or the Send Email node with an SMTP credential.

n8n → "HTTP Request" node
─────────────────────────────────
Method:          POST
URL:             https://api.sendscape.ai/v1/emails
Authentication:  Generic Credential → Header Auth
  • Name:        Authorization
  • Value:       Bearer YOUR_API_KEY
Send Body:       ON   (Body Content Type: JSON)
Specify Body:    Using JSON

JSON body:
{
  "from": "hello@yourdomain.com",
  "to": ["recipient@example.com"],
  "subject": "Hello from n8n",
  "html": "<p>Sent from an n8n workflow.</p>"
}

Webhooks

Register an HTTPS endpoint under Webhooks and Sendscape POSTs a signed JSON event every time one of your emails moves - delivered, bounced, complained and more. Answer any 2xx within 10 seconds; failures are retried for about a day.

EventWhenExtra fields in data
email.sentThe mail servers accepted the message.-
email.deliveredThe receiving server took it (once per message).delivery.timestamp, delivery.smtp_response, delivery.recipients
email.delivery_delayedTemporarily refused; the provider is retrying. One event per retry notice; settles to delivered or bounced.delay.type, delay.expires_at, delay.recipients[{address, status, diagnostic_code}], delay.timestamp
email.bouncedRefused by the receiving server. bounce.type Permanent = the address is now suppressed; Transient = soft bounce (mailbox full, expired), nothing suppressed.bounce.type, bounce.sub_type, bounce.diagnostic_code, bounce.recipient, bounce.timestamp
email.complainedMarked as spam; the address is suppressed.complaint.feedback_type, complaint.recipient, complaint.timestamp
email.failedThe mail provider returned an error (quota refunded), or later dropped the message as malicious content (not refunded).error
email.openedThe recipient loaded the tracking pixel (HTML mail, domain with open tracking on). One event per open.open.ip, open.user_agent, open.timestamp, open.bot
email.clickedThe recipient followed a tracked link (domain with click tracking on). One event per click.click.link, click.ip, click.user_agent, click.timestamp, click.bot
suppression.createdAn address was added to your suppression list (automatically or by you).data is {id, address, reason, detail, created_at}
suppression.deletedYou removed an address from the list.same shape

A delivery

POST https://example.com/webhooks/sendscape
Content-Type: application/json
User-Agent: Sendscape-Webhooks/1.0
Sendscape-Event: email.bounced
Sendscape-Event-Id: evt_3f9c2a1b7d4e5f60718293a4b5
Sendscape-Delivery-Id: 1187
Sendscape-Timestamp: 1789000000
Sendscape-Signature: v1=8a4d1c...

{
  "type": "email.bounced",
  "id": "evt_3f9c2a1b7d4e5f60718293a4b5",
  "created_at": "2026-09-07T10:15:09+08:00",
  "data": {
    "id": 42,
    "message_id": "0107019064ab1234-56c78d90-...",
    "from": "hello@yourdomain.com",
    "to": ["user@example.com"],
    "cc": [],
    "bcc": [],
    "reply_to": [],
    "subject": "Your receipt",
    "tags": [{ "name": "category", "value": "receipt" }],
    "status": "bounced",
    "error": null,
    "channel": "api",
    "created_at": "2026-09-07T10:15:02+08:00",
    "last_event_at": "2026-09-07T10:15:09+08:00",
    "bounce_type": "Permanent",
    "opened_at": null,
    "clicked_at": null,
    "open_count": 0,
    "click_count": 0,
    "bounce": {
      "type": "Permanent",
      "sub_type": "General",
      "diagnostic_code": "smtp; 550 5.1.1 user unknown",
      "recipient": "user@example.com",
      "timestamp": "2026-09-07T02:15:09.000Z"
    }
  }
}

Verify the signature

Sendscape-Signature is v1= plus HMAC-SHA256 over {timestamp}.{raw body} with your endpoint secret. Hash the exact bytes you received, compare in constant time, and reject timestamps more than 5 minutes off.

$secret = getenv('SENDSCAPE_WEBHOOK_SECRET');
$body = file_get_contents('php://input');           // the raw bytes, never re-encoded
$timestamp = $_SERVER['HTTP_SENDSCAPE_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_SENDSCAPE_SIGNATURE'] ?? '';

$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);

if (abs(time() - (int) $timestamp) > 300 || ! hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);
// $event['type'] e.g. "email.bounced"; $event['data'] is the email object
http_response_code(200);

Retries

AttemptDelay after the previous failure
1immediately
21 minute
35 minutes
415 minutes
51 hour
62 hours
74 hours
88 hours (last; ~26 hours in total)

After 50 consecutive final failures the endpoint is switched off and you are emailed; re-enable it from the dashboard once it is reachable. Logs are kept 30 days on Free and 90 on paid plans; webhook delivery history for 30.

Broadcasts (marketing)

Broadcasts send one email to your whole audience - newsletters, promos, announcements. Everything happens in the dashboard (no API needed): build your list under Contacts, compose and send under Broadcasts.

1
Add your contacts
Add contacts one by one or import a CSV (a header row with an email column is enough - name columns are picked up automatically, and every other column becomes a custom property like city or plan). Organize them with your own groups like “FB Leads” or “Customers”, or build a segment - a saved rule such as “city is Kuala Lumpur and not in group Customers” that is evaluated live every time you send.
2
Compose, preview, test
Pick a from-address on one of your verified domains, then write in the rich-text editor - or paste your own HTML. Personalise with merge tags ({{first_name|there}}, or any property key) from the Insert tag menu. Preview shows exactly what one real contact will receive; Send test delivers it to your own inbox, marked [TEST], with the unsubscribe link disarmed. Target all contacts, groups or a segment - the live counter shows exactly how many people will receive it.
3
Send now or schedule
Send immediately, or pick a date and time (5 minutes to 30 days out, in your account's timezone). A scheduled broadcast can be cancelled back to a draft any time before it fires. Sending runs in the background at a safe pace; the broadcast page shows live progress and a per-recipient list with real delivery statuses - Delivered, Bounced, Complained - as receiving servers report back.

Merge tags

TagRenders as
{{first_name}}The contact's first name
{{last_name}}The contact's last name
{{email}}The contact's email address
{{unsubscribe_url}}Their personal unsubscribe link (HTML mode - otherwise the footer carries it)
{{city}}Any custom property, by its key - set on the contact or imported as an extra CSV column
{{first_name|there}}A fallback after the pipe is used when the value is blank

Tags work in the subject and the body. Contact values are HTML-escaped in the body, so a name like <b>Ana</b> shows as text rather than becoming markup; a fallback is your own template and is left as written. An unknown tag with no fallback renders as nothing, never as raw braces.

Unsubscribe, handled for you

Every broadcast automatically carries an unsubscribe footer link and the one-click unsubscribe header that Gmail and Yahoo require from bulk senders - there is nothing to configure. When someone unsubscribes, future broadcasts skip them automatically, but they still receive your transactional email (receipts, OTPs) - unsubscribes and the suppression list are deliberately separate. A recipient who marks a broadcast as spam is unsubscribed automatically too.

Rules of the road

  • Broadcasts go only to subscribed contacts, deduplicated across groups.
  • A test send is a real email through your domain (it counts like any send for reputation) but writes no recipient row and never spends the free-tier monthly cap.
  • If you hold more contacts than your marketing plan allows (for example after a downgrade), adding contacts and sending broadcasts pause until you upgrade or trim the list - contacts are never deleted.
  • The free marketing tier sends up to 4,000 broadcast emails per month and adds a small “Sent with Sendscape” line to the footer - both lifted on paid marketing plans.

Plans, quotas and extras

Transactional plans

PlanPriceEmails / monthEmails / dayDomains
FreeRM03,0001001
StarterRM69/mo50,0005,00010
GrowthRM299/mo100,00010,00050

Quotas count messages, not recipients - one send to 50 addresses uses 1 from your quota (and all 50 share the same To field; see the to field above). The daily quota resets at midnight (Malaysia time); the monthly quota resets on your billing anniversary, not the calendar month. Sends over quota return 429 quota_exceeded and nothing is queued - retry after the reset. Manage your plan under Usage & Billing.

Marketing plans

PlanPriceContactsBroadcast sending
FreeRM01,0004,000 emails / month
StarterRM129/mo5,000Unlimited
GrowthRM449/mo25,000Unlimited

Marketing is priced by contacts stored, not emails sent - broadcasts never consume your transactional quota. The two plans are separate and mix freely: free transactional with a paid marketing plan works, and so does the reverse.

Extra emails (pay-as-you-go)

On paid transactional plans you can keep sending past your monthly limit: enable Extra emails under Usage & Billing → Extras, and each additional 1,000 emails deducts RM 4.00 from your credits automatically. Your daily limit always applies, and nothing is charged unless you opt in. The free plan is a hard stop at its monthly quota.

Send statuses

Every message gets a status on the Email Metrics page. Most sends start as Accepted and move on once the receiving server reports back.

StatusWhat it means
AcceptedWe handed the message to the mail servers and it is on its way. This is the normal result of a successful send.
DelayedThe receiving server refused the message for now (mailbox full, greylisting, a busy server) and the mail provider is retrying for up to 14 hours. It settles to delivered or bounced; nothing is suppressed. Each retry notice fires an email.delivery_delayed webhook.
DeliveredThe receiving mail server confirmed it took the message for the recipient. Most sends go from Accepted to Delivered within seconds.
BouncedThe receiving server refused the message. A permanent bounce (the address does not exist, the mailbox is closed) adds the address to your suppression list automatically. A soft bounce (mailbox full, message expired in transit) does not - the email is marked bounced and you may send to the address again later. Webhooks carry bounce.type to tell them apart.
ComplainedThe recipient marked the email as spam. The address is suppressed automatically, and a high complaint rate can pause your account.
RejectedSendscape refused the message before it left - over quota, a suppressed recipient, an unverified sending domain, or a paused account. Nothing was sent and no quota was used.
FailedThe mail provider returned an error while we were sending. Your quota is refunded, so it is safe to retry.

The Failed tab lists both Failed and Rejected messages - they are the two ways a send can not go out. Click any row to open its details, which include the exact reason. The codes behind a rejection are the same ones the API returns: see Errors.

Bounced and Complained addresses go straight onto your suppression list - see Suppressions and deliverability for what that means for your account.

Broadcast emails appear here too (channel broadcast). Each broadcast's own page adds two fan-out states you will not see in Email Metrics: Pending (queued, not yet sent) and Skipped (the contact unsubscribed or was suppressed by the time we reached them).

Suppressions and deliverability

When an email hard-bounces or the recipient marks it as spam, that address is automatically added to your suppression list. Later sends to it are rejected with recipient_suppressed instead of being silently dropped, so your logs stay honest. Review and manage the list under Suppressions.

To protect deliverability for everyone, accounts with a bounce rate above 5% or a complaint rate above 0.1% (measured after 50 sends) are paused automatically.

Keeping your rates healthy is mostly list hygiene:

  • Send only to recipients who opted in - never to purchased or scraped lists.
  • Remove addresses that have not engaged in months before large sends.
  • Include a plain-text part alongside your HTML.
  • Use a subdomain (like mail.yourdomain.com) for transactional mail to isolate reputation.
  • Broadcasts count toward the same bounce and complaint rates - a spam complaint on a newsletter hurts your account exactly like one on a receipt (and auto-unsubscribes that contact).

Using AI coding assistants

If you build with Claude, Cursor or a similar assistant, install the Sendscape skill and it will write integration code against the real API instead of guessing. The bundle is generated from this page, so the endpoint, request fields and error codes are always the ones documented above.

Download sendscape-skill.zip

# Install the Sendscape skill into your project
mkdir -p .claude/skills
curl -L https://sendscape.ai/sendscape-skill.zip -o /tmp/sendscape-skill.zip
unzip -o /tmp/sendscape-skill.zip -d .claude/skills

# Or install it for every project on this machine:
#   unzip -o /tmp/sendscape-skill.zip -d ~/.claude/skills

# Then just ask, and it loads on its own:
#   "send a welcome email with Sendscape when a user signs up"