API documentation

Send email with one POST.

The SendHustle API sends from domains you verify, tracks opens and clicks, and streams signed webhooks back to your app. Everything the dashboard does is available over HTTP — 97 endpoints across 13 resources.

Base URL: https://api.sendhustle.com

curl -X POST https://api.sendhustle.com/emails \
  -H "Authorization: Bearer sh_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "you@yourdomain.com",
    "to": "customer@example.com",
    "subject": "Your order is confirmed",
    "html": "<p>Thanks for your order!</p>"
  }'

The same call from Node — no SDK required:

const res = await fetch('https://api.sendhustle.com/emails', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer sh_YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    from: 'you@yourdomain.com',
    to: 'customer@example.com',
    subject: 'Your order is confirmed',
    html: '<p>Thanks for your order!</p>',
  }),
});

const data = await res.json();
console.log(data);

Authentication

Every request carries an API key as a bearer token. Create keys in the dashboard under API Keys. Keys start with sh_ and are shown once — store them in your server environment, never in client-side code.

Authorization: Bearer sh_YOUR_API_KEY

Keys carry scopes — full_access for everything, or sending_access for send-only integrations — and can be restricted to a single verified domain. Send a repeated request safely by including an Idempotency-Key header: a retry with the same key returns the original result instead of sending twice.

Endpoints

All paths are relative to https://api.sendhustle.com. Requests and responses are JSON; list endpoints use cursor pagination.

Emails

POST/emailsSend an email (transactional or marketing). Supports attachments, scheduled_at, tags, headers, reply_to, Idempotency-Key.
POST/emails/batchSend up to 100 emails in one request.
GET/emailsList sent emails (cursor pagination).
GET/emails/:idRetrieve a sent email with its delivery + engagement events.
PATCH/emails/:idReschedule a scheduled email (scheduled_at).
POST/emails/:id/cancelCancel a scheduled email.
GET/emails/:id/attachmentsList a sent email’s attachments.
GET/emails/:id/attachments/:attachment_idRetrieve one attachment of a sent email.

Received emails (inbound)

GET/emails/receivingList received emails.
GET/emails/receiving/:idRetrieve a received email (body, headers, metadata).
GET/emails/receiving/:id/rawDownload the raw MIME of a received email.
GET/emails/receiving/:id/attachmentsList a received email’s attachments.
GET/emails/receiving/:id/attachments/:attachment_idDownload one received attachment (binary).
POST/emails/receiving/:id/forwardForward a received email to another address.
DELETE/emails/receiving/:idDelete a received email.

Domains

POST/domainsAdd a sending domain (returns the DNS records to publish).
GET/domainsList sending domains.
GET/domains/:idRetrieve a domain and its DNS records.
PATCH/domains/:idUpdate tracking / TLS / return-path settings.
POST/domains/:id/verifyRe-check DNS verification.
DELETE/domains/:idDelete a domain.
POST/domains/claimClaim a domain verified by another team.
GET/domains/:id/claimRetrieve a domain’s claim record.
POST/domains/:id/claim/verifyVerify a domain claim.

Audiences & Contacts

POST/audiencesCreate an audience.
GET/audiencesList audiences.
GET/audiences/:idRetrieve an audience.
DELETE/audiences/:idDelete an audience.
POST/audiences/:audience_id/contactsAdd a contact to an audience.
GET/audiences/:audience_id/contactsList an audience’s contacts.
GET/audiences/:audience_id/contacts/:idRetrieve a contact (by id or email).
PATCH/audiences/:audience_id/contacts/:idUpdate a contact.
DELETE/audiences/:audience_id/contacts/:idRemove a contact from an audience.
POST/audiences/:audience_id/contacts/batchBulk-create contacts (JSON array).
POST/audiences/:audience_id/contacts/importBulk-import contacts from CSV.
POST/contactsCreate a contact in a domain’s pool (`domain` required).
GET/contactsList a domain’s contacts (`?domain=` required).
GET/contacts/:idRetrieve a contact (by id or email).
PATCH/contacts/:idUpdate a contact.
DELETE/contacts/:idDelete a contact.
POST/contacts/:id/segments/:segment_idAdd a contact to a segment.
DELETE/contacts/:id/segments/:segment_idRemove a contact from a segment.
GET/contacts/:id/segmentsList the segments a contact belongs to.
GET/contacts/:id/topicsRetrieve a contact’s topic subscriptions.
PATCH/contacts/:id/topicsUpdate a contact’s topic subscriptions.

Contact properties

POST/contact-propertiesCreate a custom contact property (key, type, fallback).
GET/contact-propertiesList contact properties.
GET/contact-properties/:idRetrieve a contact property.
PATCH/contact-properties/:idUpdate a contact property.
DELETE/contact-properties/:idDelete a contact property.

Segments

POST/segmentsCreate a segment on a domain (`domain` required; names unique per domain).
GET/segmentsList a domain’s segments (`?domain=` required).
GET/segments/:idRetrieve a segment.
GET/segments/:id/contactsPreview the contacts a segment resolves to.
PATCH/segments/:idUpdate a segment.
DELETE/segments/:idDelete a segment.

Topics

POST/topicsCreate a topic on a domain (`domain` required; default_subscription, visibility).
GET/topicsList a domain’s topics (`?domain=` required).
GET/topics/:idRetrieve a topic.
PATCH/topics/:idUpdate a topic.
DELETE/topics/:idDelete a topic.

Campaigns

POST/campaignsCreate a campaign targeting a domain’s contacts (`domain` required; narrow with segment / topic).
GET/campaignsList campaigns.
GET/campaigns/:idRetrieve a campaign.
PATCH/campaigns/:idUpdate a draft campaign.
POST/campaigns/:id/sendSend now, or schedule with scheduled_at.
POST/campaigns/:id/cancelCancel a scheduled campaign (back to draft).
GET/campaigns/:id/statsPerformance: delivered / open / click / unsubscribe rates.
DELETE/campaigns/:idDelete a campaign.

Templates

POST/templatesCreate a template with variables.
GET/templatesList templates.
GET/templates/:idRetrieve a template.
PATCH/templates/:idUpdate a template (creates a draft).
POST/templates/:id/duplicateDuplicate a template.
POST/templates/:id/publishPublish a template (required before use in automations).
DELETE/templates/:idDelete a template.

Automations & Events

POST/automationsCreate an event-triggered automation (steps + connections).
GET/automationsList automations.
GET/automations/:idRetrieve an automation (with steps + enrollments).
PATCH/automations/:idUpdate name / status / connections.
POST/automations/:id/stepsAppend a step.
DELETE/automations/:id/steps/:step_idDelete a step.
GET/automations/:id/runsList automation runs.
GET/automations/:id/runs/:run_idRetrieve a single run.
POST/automations/:id/stopStop an automation.
DELETE/automations/:idDelete an automation.
POST/eventsSend a custom event automations can trigger on.

Webhooks

POST/webhooksCreate a webhook (endpoint + event types). Returns the signing secret once.
GET/webhooksList webhooks.
GET/webhooks/:idRetrieve a webhook (incl. signing secret).
PATCH/webhooks/:idUpdate endpoint / events / status.
DELETE/webhooks/:idDelete a webhook.

Logs

GET/logsList API request logs.
GET/logs/:idRetrieve a single request log (with bodies).

API keys

POST/api-keysCreate an API key (full_access or sending_access).
GET/api-keysList API keys.
DELETE/api-keys/:idRevoke an API key.

Webhooks

Register an endpoint under Webhooks and we POST a JSON body for each event. Deliveries are retried with backoff, so your handler should be idempotent.

email.sentThe message was accepted by the sending infrastructure.
email.deliveredThe receiving mail server accepted the message.
email.openedA recipient opened the message (tracking pixel).
email.clickedA recipient clicked a tracked link.
email.bouncedDelivery failed permanently or transiently.
email.complainedA recipient marked the message as spam.
email.suppressedThe recipient was added to your suppression list.
domain.updatedA sending domain's verification status changed.

Verifying the signature

Each delivery carries X-SendHustle-Timestamp and X-SendHustle-Signature — an HMAC-SHA256, in hex, of <timestamp>.<raw body> using your endpoint's signing secret. Compare with a constant-time check and reject timestamps outside a few minutes to prevent replays. Svix-compatible headers are sent alongside if you prefer that library.

import crypto from 'crypto';

const ts = req.headers['x-sendhustle-timestamp'];
const sig = req.headers['x-sendhustle-signature'];
const expected = crypto
  .createHmac('sha256', process.env.WEBHOOK_SECRET)
  .update(`${ts}.${rawBody}`)
  .digest('hex');

const ok = crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));

Errors

Errors return the matching HTTP status with a JSON body containing a stable name and a human-readable message.

400validation_errorThe request body failed validation — the message names the field.
401missing_api_keyNo API key was supplied in the Authorization header.
401invalid_api_keyThe API key is unknown, revoked, or malformed.
403restricted_api_keyThe key lacks the scope (or domain) this call requires.
404not_foundThe referenced resource does not exist under your account.
422invalid_accessThe request is understood but not permitted in this state.
429rate_limit_exceededToo many requests, or your plan's sending allowance is used up.
500internal_errorSomething failed on our side — safe to retry with the same Idempotency-Key.

Rate limits

Sending is paced to protect deliverability for everyone: requests beyond the current window are queued rather than dropped, so a large campaign takes longer instead of failing. Your plan's monthly allowance is enforced separately — when it's used up, sending pauses until the next cycle or an upgrade, and we never bill silent overages. New accounts start with a lower daily allowance that lifts automatically as your sending history stays clean.

Ready to send?

Create an account, verify a domain, and generate a key — the free plan needs no card.