Hestia Signals
API Reference · v2026-06-01

The API

A unified REST API for lifecycle email and SMS, audience management, real-time data validation, and the retention signals that tell you which accounts are about to churn — built for high-volume senders.

Introduction

The API is organized around predictable, resource-oriented URLs, returns JSON-encoded responses, and uses standard HTTP verbs, status codes, and authentication. It is designed for production traffic: every endpoint is idempotent where it should be, paginated where it returns lists, and instrumented with a request ID you can trace.

Everything you can do in the dashboard you can do through the API: send and schedule messages, sync contacts, validate data before you send, stream delivery events, and read churn-risk signals to drive automated win-back.

Core principles

  • HTTPS only. Requests over plain HTTP are refused.
  • JSON in, JSON out. Send Content-Type: application/json.
  • Stable versions. Pinned per request; breaking changes never reach a pinned version.
  • Safe retries. Mutating calls accept an idempotency key.
BASE URL
https://api.hestiasignals.com/v1
A typical request
curl https://api.hestiasignals.com/v1/messages/email \
  -H "Authorization: Bearer sk_live_•••" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: a1b2c3" \
  -d '{ "to": "ava@acme.com", ... }'

Quickstart

Send your first message in under a minute. Grab a test key from Settings → API keys, then make the call on the right. Test keys never deliver to real inboxes, so you can run this safely.

1 · Send a message

Authenticate with your key, set a recipient, and send. The response returns a message id you can use to retrieve status or correlate webhook events.

2 · Listen for events

Point a webhook at your server to receive delivered, opened, and bounced events as they happen.

Send a message
curl https://api.hestiasignals.com/v1/messages/email \
  -H "Authorization: Bearer sk_test_•••" \
  -d '{ "to":"ava@acme.com",
        "from":"team@news.brand.com",
        "subject":"Welcome aboard",
        "template_id":"tmpl_welcome" }'

Authentication

Authenticate with your secret key as a bearer token in the Authorization header. Keys are environment-scoped: sk_live_… sends real traffic, sk_test_… runs in a sandbox. Create, roll, and scope keys in the dashboard.

Key permissions

Restricted keys can be limited to specific resources — for example a key that may send messages but not read contacts — so a leaked key never exposes more than its scope.

!
Secret keys carry account access. Never embed them in client-side code, mobile apps, or public repositories, and never pass them in a query string. Roll a key the moment it is exposed.
Authorization header
Authorization: Bearer sk_live_4eC39HqLyjWDarjtT1zdp7dc
401 Unauthorized
{
  "error": {
    "type": "authentication_error",
    "message": "No valid API key provided."
  }
}

Requests & idempotency

All POST requests accept an Idempotency-Key header. If a request is interrupted — a dropped connection, a timeout, a retry — replaying it with the same key returns the original result instead of performing the action twice. Keys are stored for 24 hours.

Generate a unique key per logical operation (a UUID works well). Reusing a key with a different body returns a 409, which protects you from accidental double-sends at scale.

Metadata

Attach a metadata object of up to 50 key–value pairs to most resources. It is never used by the system and is returned verbatim — ideal for storing your own IDs.

Safe retry
curl https://api.hestiasignals.com/v1/messages/email \
  -H "Authorization: Bearer sk_live_•••" \
  -H "Idempotency-Key: 9f1c-44a2-8b30" \
  -d '{ "to": "ava@acme.com", ... }'

# Replaying with the same key returns
# the same msg_… instead of sending twice.

Versioning

The API is versioned by date. Your account is pinned to the version that was current when you integrated, and that behavior never changes underneath you. Upgrade deliberately by setting the Version header, test against it, then make it your default in the dashboard.

Additive changes — new endpoints, new optional fields, new event types — ship without a version bump, so write parsers that ignore unknown fields.

Pin a version per request
-H "Version: 2026-06-01"

Pagination

List endpoints are cursor-paginated. Pass limit (1–100, default 20) and walk forward with starting_after, using the last object's id from the previous page. The envelope's has_more tells you when to stop — cursors stay stable even as new records are created.

Parameters

limitintegeroptional
Objects per page, 1–100. Defaults to 20.
starting_afterstringoptional
An object ID that defines your place in the list.
ending_beforestringoptional
An object ID to paginate backward from.
List envelope
{
  "object": "list",
  "url": "/v1/contacts",
  "has_more": true,
  "data": [ { "id": "con_5kT1",} ]
}
Next page
curl "https://api.hestiasignals.com/v1/contacts?limit=20&starting_after=con_5kT1"

Rate limits

Limits apply per key and are returned on every response so you can throttle before you are throttled. A burst allowance absorbs short spikes; sustained traffic above your steady rate returns 429 with a Retry-After. Wait for the time it indicates, then retry with exponential backoff.

X-RateLimit-Limit
Requests allowed in the current window.
X-RateLimit-Remaining
Requests left before throttling.
Retry-After
Seconds to wait after a 429.
Steady-state limits
# requests / second   burst
Starter       10          20
Growth        50         100
Scale        200         400
Enterprise   custom       custom

Errors

The API uses conventional HTTP status codes. 2xx means success, 4xx indicates a request problem you can fix, and 5xx indicates a rare error on our side. Every error body is typed, carries a stable code, names the offending param, and includes a request_id for support.

Error types

invalid_request400Malformed or missing parameters.
authentication401Missing or invalid API key.
permission403Key lacks scope for this resource.
not_found404Resource does not exist.
conflict409Idempotency key reused with a different body.
validation422A field failed validation.
rate_limit429Too many requests.
api_error500Something went wrong on our end.
Error object
{
  "error": {
    "type": "validation",
    "code": "invalid_email",
    "message": "'to' is not a valid email.",
    "param": "to",
    "doc_url": "https://docs.../invalid_email",
    "request_id": "req_8sK2c91Lp0"
  }
}

Test mode

Every endpoint works identically with a test key, but nothing is delivered and nothing is billed. Use the magic recipients below to force outcomes deterministically, so your integration tests can assert on bounces and complaints without touching a real inbox.

bounce@test.hestiasignals.com
Always produces a bounced event.
complaint@test.hestiasignals.com
Always produces a complained event.
success@test.hestiasignals.com
Always delivered then opened.
Force a bounce
curl https://api.hestiasignals.com/v1/messages/email \
  -H "Authorization: Bearer sk_test_•••" \
  -d '{ "to": "bounce@test.hestiasignals.com", ... }'

Send an email

POST/v1/messages/email

Sends a transactional or lifecycle email to one recipient. Suppressed addresses are skipped and returned with a suppressed status — they never count against deliverability.

Body parameters

tostringrequired
Recipient email address.
fromstringrequired
A verified sender on one of your domains.
subjectstringrequired
Supports merge variables like {{first_name}}.
htmlstringoptional
Raw HTML body. Provide either html or template_id.
template_idstringoptional
ID of a saved, version-controlled template.
variablesobjectoptional
Merge data injected into the subject, body, or template.
tagsarrayoptional
Labels for analytics and segmentation.
send_atstringoptional
ISO 8601 timestamp to schedule the send.
metadataobjectoptional
Up to 50 key–value pairs, returned verbatim.
Request
curl https://api.hestiasignals.com/v1/messages/email \
  -H "Authorization: Bearer sk_live_•••" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "ava@acme.com",
    "from": "team@news.brand.com",
    "subject": "We saved your seat, {{first_name}}",
    "template_id": "tmpl_winback_02",
    "variables": { "first_name": "Ava" },
    "tags": ["win-back"]
  }'
201 Created
{
  "id": "msg_3aZ91kP2",
  "object": "email",
  "status": "queued",
  "to": "ava@acme.com",
  "created_at": "2026-06-12T09:24:11Z"
}

Send an SMS

POST/v1/messages/sms

Sends an SMS to a single E.164 number. Long messages are split into segments automatically; the response reports the segment count so you can reconcile usage.

Body parameters

tostringrequired
Recipient in E.164, e.g. +35799123456.
fromstringrequired
An approved sender ID or number.
bodystringrequired
Message text; supports merge variables.
send_atstringoptional
ISO 8601 timestamp to schedule.
Request
curl https://api.hestiasignals.com/v1/messages/sms \
  -H "Authorization: Bearer sk_live_•••" \
  -d '{
    "to": "+35799123456",
    "from": "BRAND",
    "body": "Your cart is still here, Ava → https://r.tv/x"
  }'
201 Created
{
  "id": "sms_7Bq02Xm9",
  "status": "queued",
  "segments": 1
}

Batch send

POST/v1/messages/batch

Queue up to 1,000 messages in a single call. The batch is accepted atomically and processed asynchronously; you receive a batch_id immediately and per-message events stream to your webhook as they send. Ideal for a scheduled win-back blast without hammering the single-send endpoint.

Request
curl https://api.hestiasignals.com/v1/messages/batch \
  -H "Authorization: Bearer sk_live_•••" \
  -d '{
    "template_id": "tmpl_winback_02",
    "messages": [
      { "to": "ava@acme.com",  "variables": {"first_name":"Ava"} },
      { "to": "ben@globex.com", "variables": {"first_name":"Ben"} }
    ]
  }'
202 Accepted
{ "batch_id": "batch_K2p9", "queued": 2 }

Retrieve a message

GET/v1/messages/{id}

Returns the current state of a message, including its delivery timeline. Status transitions are queued → sent → delivered, with bounced, complained, or suppressed as terminal outcomes.

200 OK
{
  "id": "msg_3aZ91kP2",
  "status": "delivered",
  "events": [
    { "type":"sent", "at":"…:11Z" },
    { "type":"delivered", "at":"…:14Z" }
  ]
}

Create a contact

POST/v1/contacts

Creates or updates a contact, keyed on email — calling it again upserts, so it is safe to run inside a sync loop. Consent is first-class: record per-channel opt-in here, and the system enforces it on every send.

Body parameters

emailstringrequired
Unique key for the contact.
phonestringoptional
E.164 number for SMS.
first_namestringoptional
Given name.
consentobjectoptional
Per-channel opt-in, e.g. { "email": true, "sms": false }.
attributesobjectoptional
Custom fields you segment on (plan, MRR, signup date).
Request
curl https://api.hestiasignals.com/v1/contacts \
  -H "Authorization: Bearer sk_live_•••" \
  -d '{
    "email": "ava@acme.com",
    "first_name": "Ava",
    "consent": { "email": true, "sms": false },
    "attributes": { "plan": "growth", "mrr": 799 }
  }'

List contacts

GET/v1/contacts

Returns a cursor-paginated list, filterable by attribute, consent, and creation date. Combine filters to build any segment server-side — for example all growth-plan contacts who opted into email and were created this quarter.

attributes[plan]stringoptional
Filter by any custom attribute.
consent[email]booleanoptional
Only contacts with the given consent state.
created[gte]stringoptional
ISO 8601 lower bound on creation time.
Filtered list
curl -G https://api.hestiasignals.com/v1/contacts \
  -H "Authorization: Bearer sk_live_•••" \
  --data-urlencode "attributes[plan]=growth" \
  --data-urlencode "consent[email]=true" \
  --data-urlencode "limit=50"

Delete a contact

DELETE/v1/contacts/{id}

Permanently erases a contact and its message history — a hard delete that satisfies a GDPR erasure request. The address is added to your suppression list so it cannot be re-imported by accident. This action cannot be undone.

200 OK
{ "id": "con_5kT1", "deleted": true }

Validate an address

POST/v1/validate

Runs real-time syntax, domain, mailbox, and risk checks on an email or phone number, flagging spam traps and catch-all domains before you ever send. Wire this into your signup form to keep junk out of your list at the source.

Response fields

resultstring
deliverable, risky, or undeliverable.
scorenumber
Confidence from 0–100.
is_spam_trapboolean
Matches a known trap.
is_catch_allboolean
Domain accepts all addresses.
200 OK
{
  "email": "ava@acme.com",
  "result": "deliverable",
  "score": 97,
  "is_spam_trap": false,
  "is_catch_all": false
}

Bulk validation job

POST/v1/validate/jobs

Validate an entire list asynchronously. Submit up to one million records, poll the job (or wait for the job.completed webhook), then download a results file. The job reports live progress so you can show a meter in your own UI.

202 Accepted
{
  "id": "job_Vd83",
  "status": "processing",
  "total": 240000,
  "processed": 18250
}

Retention signals

GET/v1/signals

This is the intelligence layer. The model scores every account for churn risk and exposes the factors behind each score, so you can trigger the right intervention while the account is still savable — not read about it in a quarterly report. Filter by risk band and feed the result straight into a journey.

riskstringoptional
low, medium, or high.
limitintegeroptional
1–100, default 20.
200 OK
{
  "object": "list",
  "data": [ {
    "contact_id": "con_5kT1",
    "risk": "high",
    "score": 88,
    "factors": [
      "no_login_30d",
      "support_tickets_up",
      "usage_down_60pct"
    ]
  } ]
}

Webhook endpoints

POST/v1/webhook_endpoints

Register an HTTPS URL and we'll POST a signed JSON payload whenever a subscribed event fires — delivery, opens, bounces, complaints, opt-outs, and churn-risk crossings. Endpoints retry with exponential backoff for up to 24 hours until your server returns 2xx.

Verifying signatures

Every payload is signed. Compute an HMAC-SHA256 of the raw request body using your endpoint's signing secret and compare it, in constant time, to the Signature header. Reject anything that doesn't match — this is how you know the event truly came from us.

import crypto from "crypto";

function verify(rawBody, header, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(header),
    Buffer.from(expected)
  );
}
import hmac, hashlib

def verify(raw_body, header, secret):
    expected = hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(header, expected)
Payload
{
  "id": "evt_9Lm2",
  "type": "email.opened",
  "created_at": "2026-06-12T09:31:02Z",
  "data": {
    "message_id": "msg_3aZ91kP2",
    "contact": "ava@acme.com"
  }
}

Event types

Subscribe to exactly the events you need. The catalog below is stable; new types are added over time, so handle unknown types gracefully.

email.delivered
Accepted by the recipient's mail server.
email.opened
The recipient opened the message.
email.clicked
A tracked link was clicked.
email.bounced
Hard or soft bounce.
email.complained
Marked as spam by the recipient.
sms.delivered
SMS reached the handset.
contact.unsubscribed
The contact opted out.
signal.churn_risk
An account crossed into high risk.
job.completed
A bulk validation job finished.
Subscribe
curl https://api.hestiasignals.com/v1/webhook_endpoints \
  -H "Authorization: Bearer sk_live_•••" \
  -d '{
    "url": "https://hooks.yoursite.com/rtv",
    "events": ["email.bounced","signal.churn_risk"]
  }'

Suppressions

POST/v1/suppressions
DELETE/v1/suppressions/{email}

The suppression list holds addresses that must never be contacted — unsubscribes, hard bounces, and complaints are added automatically. Any send to a suppressed address is skipped at the platform level and cannot be overridden by an API call.

i
This guardrail is deliberate: it protects your sender reputation and keeps every send compliant, no matter how your integration is wired.
Add a suppression
curl https://api.hestiasignals.com/v1/suppressions \
  -H "Authorization: Bearer sk_live_•••" \
  -d '{ "email": "ava@acme.com", "reason": "manual" }'