Hivsy Docs
On this page

REST API & Webhook Reference

Public developer reference for sending email, triggering automations, managing recipient consent, and receiving signed lifecycle webhooks. No sign-in is required to read these docs.

Authentication

Create API key

Public integration requests require a live or test API key in the Authorization header.

bash
curl https://api.hivsy.com/api/emails \  -H "Authorization: Bearer hsl_live_<your_key>" \  -H "Content-Type: application/json" \  -d '{ ... }'

Keys may be scoped to one domain. A scoped key can only send from that verified domain. Dashboard endpoints use the signed-in browser session and are outside this public API contract.

Base URL

https
https://api.hivsy.com

Integration endpoints accept and return JSON. Email sends use /api/emails; the Resend-compatible alias /emails has the same request and response contract.

Errors

JSON errors contain an error field. Rate-limit responses may be plain text.

json
{  "error": "invalid from address"}
400Invalid JSON, addresses, parameters, or a suppressed recipient
401Missing or invalid API key
402Insufficient email credits
403Domain-scoped key does not match the sender domain
404Referenced template or audience not found
409An idempotency key was reused with a different request
413JSON request body exceeds the endpoint limit
423Sending is paused for this account
429Rate limit exceeded
500Unexpected server error

DNS prerequisite: verify the domain's SPF and DKIM records before using outbound lifecycle, open, or click events. To use email.received, also publish the inbound MX record shown on the domain's DNS Records tab.

Rate limits

API-key endpoints share a limit of 300 requests per minute per key. Requests above the limit return HTTP 429. Use an idempotency key when retrying sends or automation events.

OpenAPI specification

The OpenAPI 3.1 contract is public and suitable for SDK generators, API clients, and validation tools.

https
https://api.hivsy.com/api/openapi.yaml

Download the YAML specification or use /api/docs as the stable discovery route.

Send email

POST/api/emails

Queue an email from a verified domain. The response contains the email ID and initial status.

ParameterTypeDescriptionRequired
fromstringSender on a verified domain; display names are allowed.Yes
tostring[]At least one address. Maximum 50 unique recipients total across To, CC, and BCC; duplicate mailboxes are rejected case-insensitively.Yes
subjectstringRequired unless supplied by the selected template; at most 998 bytes.No
htmlstringHTML body, up to 1 MiB. Required unless text or a template body is available.No
textstringPlain-text body, up to 100 KiB. Required unless HTML or a template body is available.No
ccstring[]CC recipients.No
bccstring[]BCC recipients.No
reply_tostringReply-To address.No
headersobjectUp to 20 custom X-* headers; each pair is limited to 1 KiB.No
attachmentsobject[]Up to 10 attachments, each at most 5 MiB decoded. The complete JSON request, including base64 expansion, must remain under 10 MiB.No
template_iduuidSaved template owned by this account.No
variablesobjectString values for exact {{key}} placeholders. HTML values are escaped.No
scheduled_atdate-timeFuture RFC 3339 timestamp, no more than one year ahead.No

Attachment object

ParameterTypeDescriptionRequired
filenamestringFile name of at most 255 UTF-8 bytes, without path separators or control characters.Yes
content_typestringOptional valid MIME media type such as application/pdf.No
contentstringBase64-encoded file content.Yes
bash
curl -X POST https://api.hivsy.com/api/emails \  -H "Authorization: Bearer hsl_live_<key>" \  -H "Content-Type: application/json" \  -d '{    "from": "Hivsy <hello@yourdomain.com>",    "to": ["ada@example.com"],    "subject": "Your order is confirmed",    "html": "<p>Thanks for your order, <strong>Ada</strong>!</p>",    "text": "Thanks for your order!"  }'
json
{  "id": "5266867d-9729-49c5-adef-06c2113f292a",  "status": "queued"}
javascript
const response = await fetch('https://api.hivsy.com/api/emails', {  method: 'POST',  headers: {    Authorization: 'Bearer hsl_live_<key>',    'Content-Type': 'application/json',  },  body: JSON.stringify({    from: 'hello@yourdomain.com',    to: ['ada@example.com'],    subject: 'Welcome',    html: '<p>Hello!</p>',  }),});const { id, status } = await response.json();

Schedule delivery

Set scheduled_at to a future RFC 3339 timestamp. Scheduled sends can be queued up to one year ahead and consume credits when accepted.

bash
curl -X POST https://api.hivsy.com/api/emails \  -H "Authorization: Bearer $HIVSY_API_KEY" \  -H "Content-Type: application/json" \  -d '{    "from": "you@yourdomain.com",    "to": ["ada@example.com"],    "subject": "Your scheduled update",    "text": "This was scheduled in advance.",    "scheduled_at": "2027-01-15T15:00:00Z"  }'

Delivery retries

Temporary SMTP failures, including 4xx replies and connection errors, are retried for up to five days from the time the message becomes eligible for delivery. The first retry is scheduled after about 30 minutes, the second after about two hours, and later attempts every two to three hours. Small timing jitter prevents a recovering provider from receiving every delayed message at once.

Permanent 5xx replies are not retried. Only a permanent RCPT TO rejection is treated as a hard bounce and suppresses that exact recipient; sender, authentication, TLS, and message-transfer failures never suppress recipients.

For direct delivery to more than one recipient domain, Hivsy records each destination that already accepted the message. A later retry sends only to the remaining recipients, preventing duplicate delivery to successful domains.

The email detail page records every attempt with its SMTP stage, reply code, enhanced status, exact provider response, recipients already accepted, next retry time, and final retry deadline. Use the Events tab or copy the diagnostic report when investigating a failure.

Idempotency

Send an Idempotency-Key header of at most 256 characters. Reusing the key returns the original email with HTTP 200 and idempotent_replay: true instead of creating a duplicate.

bash
curl -X POST https://api.hivsy.com/api/emails \  -H "Authorization: Bearer $HIVSY_API_KEY" \  -H "Idempotency-Key: order-1234-receipt" \  -H "Content-Type: application/json" \  -d '{ "from": "you@yourdomain.com", "to": ["ada@example.com"], "subject": "Receipt", "text": "Thanks!" }'

Test mode

Test keys use the hsl_test_ prefix. They validate the same payload, record the email with status sent, and return HTTP 202 without delivering or consuming credits.

Suppressions

Hard bounces and one-click unsubscribes populate the suppression list automatically. A send containing any suppressed recipient is rejected with HTTP 400 before credits are charged. Manage suppressions in the dashboard or call POST /api/unsubscribe from an integration.

Trigger an automation event

POST/api/events

Record an application event and trigger active automations whose configured event name matches.

Include an Idempotency-Key header (up to 256 characters) when retrying. The same key and payload reuse the original event and automation runs; a changed payload returns HTTP 409.

ParameterTypeDescriptionRequired
namestringAutomation trigger name, for example user.signed_up.Yes
contact_emailstringRecipient used by matching email automations. Without it, the event is recorded and each matching email run is marked failed with a diagnostic.No
dataobjectApplication-defined metadata. The complete event request is limited to 1 MiB.No
bash
curl -X POST https://api.hivsy.com/api/events \  -H "Authorization: Bearer $HIVSY_API_KEY" \  -H "Idempotency-Key: customer-123-created" \  -H "Content-Type: application/json" \  -d '{    "name": "user.signed_up",    "contact_email": "ada@example.com",    "data": { "first_name": "Ada", "plan": "starter" }  }'
json
{  "event_id": "d908ca46-eef2-4572-a9dc-6b33d1842335",  "automations_triggered": 1}

Unsubscribe a contact

POST/api/unsubscribe

Unsubscribe matching contacts across all owned audiences, or scope the update to one audience.

ParameterTypeDescriptionRequired
emailstringContact email address.Yes
audience_iduuidOptional owned audience. Omit it to update every audience.No
bash
curl -X POST https://api.hivsy.com/api/unsubscribe \  -H "Authorization: Bearer $HIVSY_API_KEY" \  -H "Content-Type: application/json" \  -d '{ "email": "ada@example.com" }'
json
{  "unsubscribed": 2}

Webhook delivery

Configure webhook endpoints from a verified domain in the dashboard. Hivsy returns the signing secret when an endpoint is created or its secret is rotated. Store it immediately.

An empty event selection subscribes the endpoint to every event. Hivsy treats any 2xx response as an acknowledgement and retries connection failures or non-2xx responses up to ten times, ending after the final 24-hour retry.

json
{  "type": "email.sent",  "created_at": "2026-08-05T19:14:22Z",  "data": {    "email_id": "5266867d-9729-49c5-adef-06c2113f292a",    "from": "hello@yourdomain.com",    "to": [      "ada@example.com"    ],    "subject": "Your order is confirmed",    "created_at": "2026-08-05T19:14:19Z",    "sent_at": "2026-08-05T19:14:21Z"  }}

Webhook event types

EventStatusMeaning
email.receivedLiveInbound email received for a verified domain
email.sentLiveOutbound message accepted by the recipient server
email.failedLiveOutbound delivery permanently failed
email.delivery_delayedLiveA temporary delivery failure will be retried
email.openedLiveTracking pixel requested by a recipient
email.clickedLiveTracked link opened by a recipient
email.suppressedLiveDelivery stopped because every recipient was suppressed

Verify webhook signatures

Read the Webhook-Signature header and calculate HMAC-SHA256 over the exact raw request body. The header value is sha256=<lowercase hex digest>.X-Webhook-Signature is also sent for backward compatibility.

Verify the raw bytes before parsing JSON. Re-serializing the payload changes the signature input.

javascript
import crypto from 'node:crypto'; function verifyWebhook(rawBody, signature, secret) {  if (!signature?.startsWith('sha256=')) return false;  const actual = signature.slice('sha256='.length);  const expected = crypto    .createHmac('sha256', secret)    .update(rawBody)    .digest('hex');  if (actual.length !== expected.length) return false;  return crypto.timingSafeEqual(    Buffer.from(actual, 'hex'),    Buffer.from(expected, 'hex'),  );}
python
import hashlibimport hmac def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:    expected = "sha256=" + hmac.new(        secret.encode(), raw_body, hashlib.sha256    ).hexdigest()    return hmac.compare_digest(signature, expected)