Hollerpost API
Everything you can do from your own code or from Zapier: add contacts, send transactional email, and get a signed webhook when something happens. The base URL is https://api.hollerpost.com.
Authentication
Create a key in your dashboard under Settings, Developer API. Keys start with hp_live_ and are shown once, so copy yours when you make it. Send it as a Bearer token on every request:
Authorization: Bearer hp_live_your_key_here
A key belongs to one workspace. Treat it like a password: keep it on your server, never in a web page. You can revoke a key any time and it stops working right away.
Check your key: GET /api/v1/me
curl https://api.hollerpost.com/api/v1/me \
-H "Authorization: Bearer $HOLLERPOST_KEY"
Returns the workspace the key belongs to:
{
"tenant_id": "6f1c0b9e-...",
"workspace_name": "Sunny Side Salon",
"brand": "hollerpost",
"plan": "starter_hosted",
"trial": { "active": true, "ends_at": "2026-10-05T17:00:00Z", "emails_left": 640 }
}
Contacts
Add or update a contact: POST /api/v1/contacts
Adds someone to your list, or updates them if the email is already there. consent must be true: only add people who agreed to hear from you.
curl https://api.hollerpost.com/api/v1/contacts \
-H "Authorization: Bearer $HOLLERPOST_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "sam@example.com",
"first_name": "Sam",
"last_name": "Lee",
"tags": ["vip"],
"consent": true
}'
Returns 201 with {"id": "...", "status": "active", "created": true} for a new contact, or 200 with "created": false for an update. Someone who unsubscribed or bounced stays that way: you get 409 recipient_suppressed.
During your free trial, before your first campaign is reviewed, your list can hold up to 1,000 contacts. Past that you get 409 trial_import_cap. Picking a plan lifts it.
List and find contacts: GET /api/v1/contacts
Query parameters: email (exact match), status (subscribed or unsubscribed), tag, sort (-created_at or -updated_at), limit (up to 200), offset.
curl "https://api.hollerpost.com/api/v1/contacts?status=subscribed&sort=-created_at&limit=25" \
-H "Authorization: Bearer $HOLLERPOST_KEY"
Transactional email: POST /api/v1/emails
One email to one person: receipts, booking confirmations, password resets. It sends from your own verified domain. It counts toward your monthly sends like any other email.
curl https://api.hollerpost.com/api/v1/emails \
-H "Authorization: Bearer $HOLLERPOST_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{
"to": "sam@example.com",
"subject": "Your order is on its way",
"html": "<p>Thanks, Sam. It ships today.</p>"
}'
Optional fields: text (made from your HTML if you leave it out), replyTo, fromName, and attachments (up to 5 files as {"filename", "content"} with base64 content, about 5 MB total). Send the same Idempotency-Key again and you get the first result back instead of a second email.
Webhooks
Get a POST to your URL when something happens. Your URL must be public and https.
Events
contact.subscribed: someone joined your list (signup form, import, or the API).contact.unsubscribed: someone unsubscribed.form.submitted: someone filled in one of your forms.campaign.queued: a campaign was queued to send.email.delivered,email.bounced,email.complained: what happened to an email after it left.
Subscribe: POST /api/v1/webhooks
curl https://api.hollerpost.com/api/v1/webhooks \
-H "Authorization: Bearer $HOLLERPOST_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://example.com/hooks/hollerpost", "events": ["contact.subscribed"] }'
Returns {"id": "...", "secret": "..."}. Save the secret: it signs every delivery and isn’t shown again. List your webhooks with GET /api/v1/webhooks and remove one with DELETE /api/v1/webhooks/{id}. A workspace can have up to 25.
What you receive
POST /hooks/hollerpost
X-Utopian-Event: contact.subscribed
X-Utopian-Signature: 5d41402abc4b2a76b9719d911017c592...
{
"event": "contact.subscribed",
"data": { "email": "sam@example.com", "first_name": "Sam", "tags": ["signup-form"], "source": "signup_form" },
"fired_at": "2026-09-28T19:45:11.491Z"
}
Bounce and complaint events carry {"recipients": ["sam@example.com"]} in data.
Check the signature
X-Utopian-Signature is the HMAC-SHA256 of the raw request body, using your webhook secret, in hex. Compare it before you trust the payload.
Node:
import crypto from 'node:crypto';
function isFromHollerpost(rawBody, signature, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(String(signature || ''));
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Python:
import hashlib, hmac
def is_from_hollerpost(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature or "")
Use the body exactly as it arrived. If your framework parses JSON first and you re-serialize it, the signature won’t match.
Polling instead of webhooks
If you’d rather ask than be told: new subscribers with GET /api/v1/contacts?status=subscribed&sort=-created_at, unsubscribes with ?status=unsubscribed&sort=-updated_at, and bounces with GET /api/v1/events?type=email.bounced&limit=25. Items have the same fields as the webhook data, plus an id.
Rate limits
Each key can make 600 requests a minute. Past that you get 429 rate_limited with a Retry-After header in seconds. Emails also count toward your plan’s monthly sends, the same as campaigns.
Errors
Errors come back as JSON: {"error": "code"}, sometimes with more detail. The ones you’re most likely to see:
| Status | Code | What it means |
|---|---|---|
| 401 | missing_api_key, invalid_api_key | No key, or it was revoked. |
| 400 | consent_required | Send "consent": true when adding a contact. |
| 400 | invalid_url | Webhook URLs must be public https. |
| 402 | plan_expired, monthly_quota_exceeded | Pick a plan, or add a send pack, in your dashboard. |
| 409 | sending_domain_required | Add and verify your sending domain in Settings first. |
| 409 | trial_cap, trial_import_cap | You hit a free trial limit. Picking a plan lifts it. |
| 409 | recipient_suppressed | That person unsubscribed or bounced, so we won’t email them. |
| 429 | rate_limited | Slow down and retry after Retry-After seconds. |
Stuck? Email support@hollerpost.com. It comes to me.