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_KEYKeys 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 | /emails | Send an email (transactional or marketing). Supports attachments, scheduled_at, tags, headers, reply_to, Idempotency-Key. |
| POST | /emails/batch | Send up to 100 emails in one request. |
| GET | /emails | List sent emails (cursor pagination). |
| GET | /emails/:id | Retrieve a sent email with its delivery + engagement events. |
| PATCH | /emails/:id | Reschedule a scheduled email (scheduled_at). |
| POST | /emails/:id/cancel | Cancel a scheduled email. |
| GET | /emails/:id/attachments | List a sent email’s attachments. |
| GET | /emails/:id/attachments/:attachment_id | Retrieve one attachment of a sent email. |
Received emails (inbound)
| GET | /emails/receiving | List received emails. |
| GET | /emails/receiving/:id | Retrieve a received email (body, headers, metadata). |
| GET | /emails/receiving/:id/raw | Download the raw MIME of a received email. |
| GET | /emails/receiving/:id/attachments | List a received email’s attachments. |
| GET | /emails/receiving/:id/attachments/:attachment_id | Download one received attachment (binary). |
| POST | /emails/receiving/:id/forward | Forward a received email to another address. |
| DELETE | /emails/receiving/:id | Delete a received email. |
Domains
| POST | /domains | Add a sending domain (returns the DNS records to publish). |
| GET | /domains | List sending domains. |
| GET | /domains/:id | Retrieve a domain and its DNS records. |
| PATCH | /domains/:id | Update tracking / TLS / return-path settings. |
| POST | /domains/:id/verify | Re-check DNS verification. |
| DELETE | /domains/:id | Delete a domain. |
| POST | /domains/claim | Claim a domain verified by another team. |
| GET | /domains/:id/claim | Retrieve a domain’s claim record. |
| POST | /domains/:id/claim/verify | Verify a domain claim. |
Audiences & Contacts
| POST | /audiences | Create an audience. |
| GET | /audiences | List audiences. |
| GET | /audiences/:id | Retrieve an audience. |
| DELETE | /audiences/:id | Delete an audience. |
| POST | /audiences/:audience_id/contacts | Add a contact to an audience. |
| GET | /audiences/:audience_id/contacts | List an audience’s contacts. |
| GET | /audiences/:audience_id/contacts/:id | Retrieve a contact (by id or email). |
| PATCH | /audiences/:audience_id/contacts/:id | Update a contact. |
| DELETE | /audiences/:audience_id/contacts/:id | Remove a contact from an audience. |
| POST | /audiences/:audience_id/contacts/batch | Bulk-create contacts (JSON array). |
| POST | /audiences/:audience_id/contacts/import | Bulk-import contacts from CSV. |
| POST | /contacts | Create a contact in a domain’s pool (`domain` required). |
| GET | /contacts | List a domain’s contacts (`?domain=` required). |
| GET | /contacts/:id | Retrieve a contact (by id or email). |
| PATCH | /contacts/:id | Update a contact. |
| DELETE | /contacts/:id | Delete a contact. |
| POST | /contacts/:id/segments/:segment_id | Add a contact to a segment. |
| DELETE | /contacts/:id/segments/:segment_id | Remove a contact from a segment. |
| GET | /contacts/:id/segments | List the segments a contact belongs to. |
| GET | /contacts/:id/topics | Retrieve a contact’s topic subscriptions. |
| PATCH | /contacts/:id/topics | Update a contact’s topic subscriptions. |
Contact properties
| POST | /contact-properties | Create a custom contact property (key, type, fallback). |
| GET | /contact-properties | List contact properties. |
| GET | /contact-properties/:id | Retrieve a contact property. |
| PATCH | /contact-properties/:id | Update a contact property. |
| DELETE | /contact-properties/:id | Delete a contact property. |
Segments
| POST | /segments | Create a segment on a domain (`domain` required; names unique per domain). |
| GET | /segments | List a domain’s segments (`?domain=` required). |
| GET | /segments/:id | Retrieve a segment. |
| GET | /segments/:id/contacts | Preview the contacts a segment resolves to. |
| PATCH | /segments/:id | Update a segment. |
| DELETE | /segments/:id | Delete a segment. |
Topics
| POST | /topics | Create a topic on a domain (`domain` required; default_subscription, visibility). |
| GET | /topics | List a domain’s topics (`?domain=` required). |
| GET | /topics/:id | Retrieve a topic. |
| PATCH | /topics/:id | Update a topic. |
| DELETE | /topics/:id | Delete a topic. |
Campaigns
| POST | /campaigns | Create a campaign targeting a domain’s contacts (`domain` required; narrow with segment / topic). |
| GET | /campaigns | List campaigns. |
| GET | /campaigns/:id | Retrieve a campaign. |
| PATCH | /campaigns/:id | Update a draft campaign. |
| POST | /campaigns/:id/send | Send now, or schedule with scheduled_at. |
| POST | /campaigns/:id/cancel | Cancel a scheduled campaign (back to draft). |
| GET | /campaigns/:id/stats | Performance: delivered / open / click / unsubscribe rates. |
| DELETE | /campaigns/:id | Delete a campaign. |
Templates
| POST | /templates | Create a template with variables. |
| GET | /templates | List templates. |
| GET | /templates/:id | Retrieve a template. |
| PATCH | /templates/:id | Update a template (creates a draft). |
| POST | /templates/:id/duplicate | Duplicate a template. |
| POST | /templates/:id/publish | Publish a template (required before use in automations). |
| DELETE | /templates/:id | Delete a template. |
Automations & Events
| POST | /automations | Create an event-triggered automation (steps + connections). |
| GET | /automations | List automations. |
| GET | /automations/:id | Retrieve an automation (with steps + enrollments). |
| PATCH | /automations/:id | Update name / status / connections. |
| POST | /automations/:id/steps | Append a step. |
| DELETE | /automations/:id/steps/:step_id | Delete a step. |
| GET | /automations/:id/runs | List automation runs. |
| GET | /automations/:id/runs/:run_id | Retrieve a single run. |
| POST | /automations/:id/stop | Stop an automation. |
| DELETE | /automations/:id | Delete an automation. |
| POST | /events | Send a custom event automations can trigger on. |
Webhooks
| POST | /webhooks | Create a webhook (endpoint + event types). Returns the signing secret once. |
| GET | /webhooks | List webhooks. |
| GET | /webhooks/:id | Retrieve a webhook (incl. signing secret). |
| PATCH | /webhooks/:id | Update endpoint / events / status. |
| DELETE | /webhooks/:id | Delete a webhook. |
Logs
| GET | /logs | List API request logs. |
| GET | /logs/:id | Retrieve a single request log (with bodies). |
API keys
| POST | /api-keys | Create an API key (full_access or sending_access). |
| GET | /api-keys | List API keys. |
| DELETE | /api-keys/:id | Revoke 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.sent | The message was accepted by the sending infrastructure. |
| email.delivered | The receiving mail server accepted the message. |
| email.opened | A recipient opened the message (tracking pixel). |
| email.clicked | A recipient clicked a tracked link. |
| email.bounced | Delivery failed permanently or transiently. |
| email.complained | A recipient marked the message as spam. |
| email.suppressed | The recipient was added to your suppression list. |
| domain.updated | A 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.
| 400 | validation_error | The request body failed validation — the message names the field. |
| 401 | missing_api_key | No API key was supplied in the Authorization header. |
| 401 | invalid_api_key | The API key is unknown, revoked, or malformed. |
| 403 | restricted_api_key | The key lacks the scope (or domain) this call requires. |
| 404 | not_found | The referenced resource does not exist under your account. |
| 422 | invalid_access | The request is understood but not permitted in this state. |
| 429 | rate_limit_exceeded | Too many requests, or your plan's sending allowance is used up. |
| 500 | internal_error | Something 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.