Skip to content
hollerpost Start free
PricingGuidesIntegrationsSwitch from Mailchimpvs Mailchimp Log in Start free

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_ (keys made in an older Utopian Creations workspace start with uc_live_; both work the same way) 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": "hp_free",
  "trial": { "active": false, "ends_at": null, "emails_left": null }
}

plan is hp_free on the Free plan, or the paid plan’s ID, like hp_starter. Hollerpost has a Free plan with no time limit, so trial is always inactive; it stays in the response so older integrations keep working.

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.

Until your first campaign is reviewed, a workspace without a paid plan can hold up to 1,000 contacts added through the API. Past that you get 409 trial_import_cap. The review, or a paid 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. On a paid plan it keeps going past your plan, up to twice your monthly emails, so receipts and password resets don’t stop when a big newsletter uses up the month; past that you get 402 monthly_quota_exceeded. On Free it stops at your 1,000.

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.

Transactional text: POST /api/v1/sms

One text to one person: a booking confirmation, an appointment reminder. It needs a key with the send scope and a texting number of your own that has finished carrier verification. It counts toward your monthly texts.

curl https://api.hollerpost.com/api/v1/sms \
  -H "Authorization: Bearer $HOLLERPOST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+15555550123",
    "text": "Hi Sam, see you Tuesday at 2pm. Reply STOP to opt out."
  }'

to is a phone number. Use the + country code form; a 10 digit US number works too. text can be up to 1,000 characters. You get back {"sent": true, "segments": 1}, where segments is how many text messages it took.

When texting isn’t possible right now you get 503 sms_unavailable with a reason, so your code can fall back to email instead of failing. Reasons include sms_not_verified (no verified number yet), outside_sms_window (we only text between 9am and 8pm in your workspace’s time zone), low_sms_balance (fewer than 50 texts left this month), plan_expired and gateway_error (the carrier turned it down).

Other errors: 400 invalid_to (not a phone number we can read), 400 missing_text, 400 text_too_long, 422 recipient_suppressed (that number replied STOP; a transactional text doesn’t override it) and 409 setup_incomplete (finish your business details in the dashboard first; missing lists what’s left).

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.
  • survey.responded: someone answered one of your surveys (on the survey page or by clicking an answer in an email).

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-Hollerpost-Event: contact.subscribed
X-Hollerpost-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.

Survey answers carry the survey (id, slug, name), response_id, contact_id, campaign_id, source and answers in data.

Check the signature

X-Hollerpost-Signature is the HMAC-SHA256 of the raw request body, using your webhook secret, in hex. Compare it before you trust the payload. (Older integrations may read X-Utopian-Signature; it carries the same value and is still sent.)

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.

Surveys

Create a survey, open it, and read the results. The same key scopes apply: reading needs read, creating and editing need draft.

Create: POST /api/v1/surveys

curl https://api.hollerpost.com/api/v1/surveys \
  -H "Authorization: Bearer $HOLLERPOST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Visit feedback",
    "questions": [
      { "type": "rating", "prompt": "How was your visit?" },
      { "type": "single", "prompt": "Would you come back?",
        "options": [{ "key": "yes", "label": "Yes" }, { "key": "no", "label": "No" }] }
    ]
  }'

Returns 201 with the survey as a draft, including each question’s id. Question types: rating, nps, single, multi, text, long_text. Questions marked "one_click": true (rating, nps, single) can be asked inside an email.

Edit, open or close: PATCH /api/v1/surveys/:id

Send any of name, title, intro, questions, thank_you_message, or "status": "open" / "closed". An open survey has a public url, and campaigns that ask its questions can send.

List and results: GET /api/v1/surveys and GET /api/v1/surveys/:id/results

The list has each survey’s status, questions and response count. Results are totals only: answer counts, average rating, recommend score and the latest written answers, never who answered.

Your data: export and delete

Both need a key with the account scope.

Export: GET /api/v1/account/export

curl https://api.hollerpost.com/api/v1/account/export \
  -H "Authorization: Bearer $HOLLERPOST_KEY"

Returns contacts_csv (every contact as CSV text, with a header row), contact_count and campaigns (every campaign, including its content). It’s the same export as Settings, Your data, Export my data.

Delete the workspace: DELETE /api/v1/account

curl -X DELETE https://api.hollerpost.com/api/v1/account \
  -H "Authorization: Bearer $HOLLERPOST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"confirm": true}'

Without "confirm": true you get 400 confirmation_required and nothing changes. With it, scheduled campaigns are canceled, your plan is canceled, every API key (including this one) stops working and sending stops. The response has purge_after: the time, 30 days out, when everything is permanently deleted.

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:

StatusCodeWhat it means
401missing_api_key, invalid_api_keyNo key, or it was revoked.
400consent_requiredSend "consent": true when adding a contact.
400invalid_urlWebhook URLs must be public https.
402plan_expired, monthly_quota_exceededPick a plan, or add a send pack, in your dashboard.
409sending_domain_requiredAdd and verify your sending domain in Settings first.
409trial_import_capYour list hit the contact limit for workspaces without a paid plan, before the first campaign review. Picking a plan lifts it.
409survey_not_openThe email asks a question from a survey that isn’t open. Open it, or remove the question.
409recipient_suppressedThat person unsubscribed or bounced, so we won’t email them.
429rate_limitedSlow down and retry after Retry-After seconds.

Stuck? Email support@hollerpost.com. It comes to me.

hollerpost

Email and texts for small business, built and backed by a real person.

Questions? support@hollerpost.com

  • Pricing
  • Guides
  • Integrations
  • Switch from Mailchimp
  • Hollerpost vs Mailchimp
  • How our AI works
  • Text updates
  • Support
  • Log in
  • Privacy
  • Terms
  • Affiliates

Hollerpost is a service of Utopian Creations LLC, 850 Campus Dr #103, Daly City, CA 94015. © 2026 Utopian Creations LLC.