API reference
REST API reference
The SendHeron REST API: 56 endpoints across contacts, tags, sequences, enrollments, templates, emails, suppressions, analytics, sending domains, sender identities, and usage. Scoped bearer tokens, documented rate limits.
Driving this from a script or an agent rather than by hand? Read how the API is built for automated callers first. It covers the scope model, what the rate limits mean in practice, and why every page here has a markdown twin.
Beta members get API keys with their account. There is no public signup while the beta is invite-only, so the keys arrive in the onboarding email rather than from a self-serve settings page.
Before your first send
A new workspace cannot send anything. Two things have to exist first, in this order, and both are now scriptable:
- 1. An authenticated sending domain. Add it with
POST /sending/domains, publish the DNS records it returns, then pollPOST /sending/domains/{domainId}/verifyon a timer. - 2. A sender identity whose address sits under that domain, created with
POST /sending/identities. The first one in a workspace becomes its default.
You can do these in either order. Creating an identity on a domain that is not authenticated yet is an expected state rather than an error: the identity reports DOMAIN_UNVERIFIED and sends are refused until the domain verifies.
Until both exist, a send is refused and recorded as suppressed with the reason SENDER_NOT_CONFIGURED. The API never substitutes an address of ours: mail leaves on an address you own, or it does not leave.
Both surfaces need the sending:read and sending:write scopes, which are deliberately separate from emails:send: sending mail and changing who mail comes from are different privileges. Keys issued before these existed carry neither, so an existing integration has to add them.
Quickstart
Once a sender exists, this is a send. The official Node SDK is the shortest path and handles retries and idempotency for you; raw HTTP is the same call for every other language.
import { SendHeron } from 'sendheron';
const sendheron = new SendHeron(process.env.SENDHERON_API_KEY);
const { data, error } = await sendheron.emails.sendTemplate({
to: '[email protected]',
templateId: '550e8400-e29b-41d4-a716-446655440000',
variables: { orderId: '42' },
});
// A transport or request failure, after the SDK has already retried
// whatever was safe to retry.
if (error) throw error;
if (data.status === 'suppressed') {
// We refused to send, and data.errorMessage says why in a stable
// value such as 'HARD_SUPPRESSED'. Never retry this one.
log.warn('send refused', { reason: data.errorMessage });
} else {
// Accepted by the provider. Keep both ids: data.id reads the send
// back later, providerMessageId ties it to provider logs.
await orders.recordReceipt(data.id, data.providerMessageId);
}npm install sendheron, then read the SDK page for retries, idempotency and what it covers.
Read the Node SDK page, or carry on here for the HTTP contract underneath it.
Base URL
https://api.sendheron.com/api/v1Rate limits
Two ceilings apply to every request, both on a one-minute window. There is no hourly or daily quota. The tighter one you breach is the one that rejects the request with a 429.
- Per key
- 100 requests per minute. A few endpoints are tighter and each one says so on its page.
- Per organization
- Shared across every key you hold, so minting more keys buys no extra throughput. Split by what an endpoint costs, so a burst of reads cannot starve your sending budget: 1200 READ, 400 WRITE, 200 SEND. Each endpoint page names the one it counts against.
Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (epoch seconds at which the window rolls), so you can back off before you are rejected rather than after. The headers describe the per-key ceiling, except on a 429 caused by the per-organization ceiling, which re-stamps them with that ceiling: they always name the limit that refused the request.
Every 429 carries a Retry-After header, and a 429 never consumes an idempotency-key, so backing off and retrying the same key is always safe. The current ceilings and the monthly send pool are readable at GET /usage.
Errors
- 400
- The payload failed validation, or the request is not valid for the current state. The body's `description` names the field and says what was wrong with it in words, e.g. "domain: domain must be a bare hostname such as acme.com, with no scheme, path, or port". The `error` code is stable and safe to branch on; `description` is for the human reading the log.
- 401
- Missing or invalid API key.
- 403
- Valid key, but it does not carry the required scope. The `error` code is `apiKeys.insufficientScopes` and the `description` names both halves of the problem: "Missing required scope(s): X. This key holds: Y." Neither is a secret, since the required scopes are on this page and the held ones are your own credential, and a bare "Forbidden" costs a debugging pass to work out which of the two it was.
- 404
- No such record in this workspace. An id belonging to a different workspace returns this too, never a 403, because the API will not confirm that a record exists outside the workspace your key was issued in. Read it as "not yours or not there" rather than as "definitely gone".
- 409
- A duplicate, or an idempotency key reused with a different payload.
- 429
- Either rate-limit ceiling was exceeded.
Authentication
Every request carries a bearer token. Keys are scoped, so a key that reads contacts cannot send email unless you grant it that too. Every scope an endpoint lists is required: they are not alternatives. A valid key without the scope gets a 403, a missing or invalid one a 401.
Authorization: Bearer <YOUR_API_KEY>- contacts:read
- contacts:write
- sequences:read
- sequences:write
- templates:read
- templates:write
- analytics:read
- emails:send
- suppressions:read
- suppressions:write
- sending:read
- sending:write
Idempotency
Every endpoint that sends mail accepts an idempotency-key header, and you should send one on any request that puts mail into the world. Replaying a key returns the original response marked idempotent-replay instead of sending twice, which is what makes a network timeout safe to retry.
idempotency-key: <YOUR_UNIQUE_KEY>Keys are honoured for 24 hours, may be up to 255 characters, and are scoped to your organization and the one endpoint. Reusing a key with a different payload, or while the first request is still in flight, returns a 409. Requests without the header behave exactly as they always have.
Send outcomes
On POST /emails/send and POST /emails/send-template, a 201 is an outcome, not proof of dispatch. The 201 body is the send record: id, to, from (as resolved, so what the mail actually left as), subject (as rendered, variables filled in), templateId, status, errorMessage, providerMessageId, provider, stream, sentAt, openCount, clickCount and createdAt. The delivery fields deliveredAt and complainedAt start empty and are written by provider events after the send, which is what makes GET /emails/{id} worth reading back later. With sendAt set, the 201 body is the scheduled email instead: its id (the one DELETE /emails/scheduled/{id} takes), the sendAt it will fire at, and a status of SCHEDULED. POST /emails/send-bulk answers differently: { batchId, totalRecipients, queued, blocked }.
- 201 with
status: "sent" - The provider accepted the message. Persist
id, which is whatGET /emails/{id}reads back, andproviderMessageId. Done. Do not retry. - 201 with
status: "suppressed" - The compliance gate refused the send and recorded the refusal.
errorMessagecarries the machine-readable reason, from the table below. Never retry. The refusal is policy, not weather; fix the cause. - 503
emailSending.sendFailed - The provider refused the send. The FAILED attempt is recorded. Retry with the SAME idempotency key: a 503 releases it.
- 429
- A rate-limit ceiling refused the request before it did anything. Back off for
Retry-Afterseconds, then retry with the same idempotency key: a 429 never consumes one. - Other 4xx
- A bug in the request. The body carries
statusCode,message(a stable translation key, safe to branch on) anderror, plus adescriptionwhere one helps: a 403 names the scopes the key is missing and the ones it holds. Do not retry unchanged. Fix the request first.
Every reason a suppressed send can carry in errorMessage. Suppressed means the platform refused on policy, so none of these are retryable:
On Node, this table is a type. The official SDK returns data.status as sent or suppressed so the compiler makes you handle the refusal, exports the reasons below as SEND_BLOCK_REASONS for an exhaustive switch, and does the 429 and 503 retries described here for you.
- HARD_SUPPRESSED
- The address is on the suppression list for a bounce or a complaint. Blocks everything, organization-wide.
- CONSENT_SUPPRESSED
- The address is on the suppression list for an opt-out. Blocks marketing only, in one workspace.
- CONTACT_HARD_BLOCKED
- The contact record's status is
bouncedorcomplained. Blocks everything. - CONTACT_UNSUBSCRIBED
- The contact record's status is
unsubscribed. Blocks marketing only. - LIST_UNSUBSCRIBED
- The contact opted out of the specific list this message targets.
- ORG_SENDING_PAUSED
- Your organization's sending is paused, by the reputation monitor or by an operator. A sender-side state, nothing to do with the recipient.
- WORKSPACE_ARCHIVED
- The sending workspace is archived: its data stays readable, but nothing in it may still go out.
- SENDER_DOMAIN_UNVERIFIED
- The From domain has never been verified anywhere in your organization.
- SENDER_DOMAIN_UNVERIFIED_IN_WORKSPACE
- Your organization has verified this domain, in another workspace. Add the domain to this workspace, which is instant, rather than redoing DNS.
- SENDER_NOT_CONFIGURED
- This workspace has no sender identity, so there is no From address to send from. The setup steps at the top of the reference index are the fix.
Testing your integration
There is no sandbox mode yet. You test against the SES mailbox simulator instead, which exercises the real pipeline end to end: [email protected] delivers, and [email protected] hard-bounces, which is how you produce the suppressed outcome above on demand.
The full recipe is a guide: both addresses, the refusal branch, the cleanup call that removes the suppression you just created, and why you should never point a load test at the bounce address.
Endpoints (56)
Contacts13
Create, find and update the people you send to, plus their tags, events and engagement history.
GET/contactsPOST/contactsPUT/contactsGET/contacts/{id}PATCH/contacts/{id}DELETE/contacts/{id}GET/contacts/{id}/timelineGET/contacts/{id}/emailsPOST/contacts/{id}/tagsDELETE/contacts/{id}/tags/{tagId}POST/contacts/{id}/eventsPOST/contacts/{id}/sequences/{seqId}/enrollDELETE/contacts/{id}/sequences/{seqId}/unenroll
Sequences10
Read automation definitions, their steps, and how enrolled contacts are moving through them. Sequence sends spend the same pooled monthly allowance as campaigns and bulk sends.
Enrollments02
Pause and resume a single contact's run through a sequence.
Templates07
Reusable email bodies with variable substitution, and the marketing or transactional classification that decides how a send is treated. A template is authored either as raw HTML or as a block document, and both can be checked and previewed before anything is saved.
Emails05
Send transactional, templated and bulk email, now or at a scheduled time, and read any send back by id.
Suppressions05
The addresses the platform refuses to send to, why, and the one gate that decides it for every send path.
Analytics02
Aggregate delivery and engagement figures.
Sending domains04
Authenticate the domains a workspace may send from. Nothing leaves a workspace until one of these is verified and a sender identity sits under it.
Sender identities05
The saved From addresses a workspace sends as, and which one is its default. An identity is only usable once its domain is authenticated here.
Usage01
Where the organization stands against its plan: the monthly send pool, the transactional grace, and the rate ceilings the API enforces.
Private beta
Keys come with the beta account
Join the list and your onboarding email carries a scoped key, the base URL, and a person to ask when something in here is wrong.