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.
API keys are issued per account and scoped per key. Start a free trial to get one for your workspace.
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.
The identity only needs the domain to be added, not verified. An identity on a domain that is added but not yet verified is an expected state rather than an error: it reports DOMAIN_PENDING and sends are refused until the domain verifies. An address on a domain this workspace has not added is refused, unless your organization has verified that domain in another workspace.
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 if (data.status === 'queued') {
// Waiting for daily sending capacity or new-sender warm-up. It goes
// out by itself and turns 'sent' on this same record, so keep
// data.id and do not send it again.
await orders.recordPendingReceipt(data.id);
} 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, and the tighter one you breach is the one that rejects the request with a 429. They limit requests; how much mail goes out in a day is under Daily sending.
- 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. Each endpoint page names the one it counts against.
- 1200 READ Any GET, plus template validation and preview.
- 400 WRITE Every other mutation, including contact writes whose events may start an automatic enrollment.
- 200 SEND Anything that dispatches or schedules mail, or enrols a contact in a sequence.
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
descriptionnames 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". Theerrorcode is stable and safe to branch on;descriptionis for the human reading the log. - 401
- Missing or invalid API key.
- 403
- Valid key, but it does not carry the required scope. The
errorcode isapiKeys.insufficientScopesand thedescriptionnames 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 the API key's workspace 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.
Daily sending
Mail leaves through one shared sending account with a rolling 24-hour quota, counted per recipient (to, cc and bcc). Marketing waits once the platform reaches 80% of that quota, which keeps the rest for transactional mail; transactional waits only once the quota itself is spent.
No organization takes more than 40% of the quota: its marketing waits once everything it sent reaches that share, its transactional only once its transactional alone does. Marketing also waits above the volume the organization has proven it sends cleanly: its best day in the last 30 days times 2, and never less than 1,000 a day.
New senders warm up. Mail to an address the organization has never mailed goes out in small slices, and the next slice waits until the last one's bounces and complaints are known: 60 minutes, or 30 when people are already opening it. A slice that bounces or draws complaints past the line keeps the rest waiting until we have looked at it. Mail to people the organization already reached is never slowed by warm-up.
A send that waits is neither refused nor lost: the send route answers 201 with status queued, the reason in holdReason and the next attempt in heldUntil, and the same record (GET /emails/{id}) turns sent when it goes. Do not retry it. Attachments are never stored, so a send carrying them cannot wait: where it would, it answers 503 with emailSending.sendHeld; retry with the same idempotency key later. GET /usage reports the position under sending.
Every reason a waiting send can carry in holdReason. None of them needs a retry: the send goes out by itself once the cause clears.
- PLATFORM_DAILY_LIMIT
- The shared 24-hour quota has reached the line where marketing waits or, for transactional mail, is spent.
- ORGANIZATION_DAILY_SHARE
- Your organization has used its share of the shared quota: everything it sent counts against marketing, and its transactional mail alone against transactional.
- EARNED_VOLUME
- Marketing above the daily volume your organization has proven it sends cleanly.
- WARMUP_SAMPLE
- Warm-up: mail to an address your organization has never mailed, waiting for the slice ahead of it to be checked.
- WARMUP_SAMPLE_FAILED
- Warm-up: a slice crossed the bounce or complaint line, so mail to new addresses on that stream waits until we have looked at it.
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, holdReason and heldUntil (set only while the send waits), 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, which GET /emails/{id} does not read), 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. - 201 with
status: "queued" - The send is waiting for daily sending capacity or new-sender warm-up, and goes out by itself.
holdReasonsays why andheldUntilwhen it is next tried, and the same record turnssentwhen it goes, so persistid. Do not retry. Read it back withGET /emails/{id}. - 503
emailSending.sendFailed - The provider refused the send. The FAILED attempt is recorded. Retry with the SAME idempotency key: a 503 releases it.
- 503
emailSending.sendHeld - The send carries attachments and would have had to wait. Attachments are never stored, so it cannot wait. The FAILED attempt is recorded. Retry later 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 types data.status, so the compiler knows suppressed and queued from sent; it 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.
- SENDER_UNDER_REVIEW
- Your account is not approved as a sender yet, so only team members who have confirmed their email address and addresses on your verified domains are reachable, transactional mail included. Retrying does not help: the account owner asks for the review from the getting-started card in the dashboard, and it lifts on approval.
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.
Until your account has been approved it reaches only team members who have confirmed their email address and your verified domains, so a send to anyone else, the simulator included, comes back suppressed with SENDER_UNDER_REVIEW. The account owner asks for the review from the getting-started card in the dashboard. Test against your own confirmed address until then, and move to the simulator once GET /usage reports plan.sendingReview as APPROVED.
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: a running trial or the reason for a lapse, whether it has passed its sending review, the monthly send pool and its transactional grace, the contact cap, its place in the day's sending, and the rate ceilings the API enforces.
Free trial
Keys come with the account
Start a trial and point the reference at your own workspace. A person is one email away when something in here is wrong.
7-day free trial. Pick a plan, add a card, cancel before the trial ends and you won't be charged. 1,000 contacts and 5,000 sends during the trial.