Send a message from your server
Send one email, SMS, WhatsApp message, or push to one contact with a single request and a secret key.
Send a receipt, a password reset, or any message your backend decides on. One request sends one message to one contact, through a channel connection you already set up. About 5 minutes.
Before you start
You need an active connection for the channel: email, SMS, WhatsApp, or push. You also need a secret key that can send messages. In FounderHQ, open Settings → API Keys, choose Send messages from your server, and store the key in your server environment.
Send with the content in the request
Put the whole message in content. You do not need a template.
curl https://app.getfounderhq.com/api/v1/messages \
-H "Authorization: Bearer fhq_sk_XXXX" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: reset-user_42-2026-10-04" \
-d '{
"channel": "email",
"to": { "email": "jane@acme.com" },
"content": {
"subject": "Reset your password",
"html": "<p>Hi {{ contact.firstName | default: \"there\" }}, <a href=\"{{ vars.resetLink }}\">reset your password</a>.</p>"
},
"variables": { "resetLink": "https://app.acme.com/reset/abc123" }
}'FounderHQ answers 202 with the message. queued means FounderHQ accepted
it and sends it within moments.
{
"id": "0f8b3a52-6d0e-4c0e-9a55-0f6a2b1c7d11",
"channel": "email",
"status": "queued",
"category": "transactional",
"contactId": "c5e1a2f0-93b4-4a61-8d1e-7b9f4c2d6e10",
"to": "jane@acme.com",
"connection": "7a1c9d2e-4b3f-4e8a-b6c5-2d1e0f9a8b7c",
"template": null,
"subject": "Reset your password",
"error": null,
"createdAt": "2026-10-04T10:00:00.000Z",
"sentAt": null,
"deliveredAt": null,
"openedAt": null,
"clickedAt": null,
"bouncedAt": null
}Send with a template
Pass the template's id in template. FounderHQ sends its published version
and takes the channel from the template.
curl https://app.getfounderhq.com/api/v1/messages \
-H "Authorization: Bearer fhq_sk_XXXX" \
-H "Content-Type: application/json" \
-d '{
"to": { "externalId": "user_42" },
"template": "TEMPLATE_ID",
"variables": { "orderNumber": "1042" }
}'List your templates to find the id:
curl "https://app.getfounderhq.com/api/v1/templates?channel=email" \
-H "Authorization: Bearer fhq_sk_XXXX"Request fields
| Field | Required | What it does |
|---|---|---|
channel | With content | email, sms, whatsapp, or push |
to | Yes | The contact. See Who gets the message |
content | One of the two | The message itself. See Content for each channel |
template | One of the two | The id of a published template |
variables | No | Values the message reads as {{ vars.name }} |
connection | No | The id of the connection that sends. See Choose the connection |
category | No | transactional (default) or marketing |
Send content or template, never both.
Who gets the message
to identifies one contact. Use one of these two forms:
externalId,email,phone, or several of them. FounderHQ finds the contact, or creates one in the brand of your key. Anemailorphonethe contact did not have is saved on the contact.contactIdalone: the id of a contact FounderHQ already has.
The message goes to the contact's saved address: the email for email, the phone number for SMS and WhatsApp. Push goes to every device the contact registered. If your only push connection is a custom HTTP integration, FounderHQ sends it one request, with the device the contact used last.
email is one bare address, like jane@acme.com. FounderHQ refuses a list
of addresses and a name with an address.
If externalId and email belong to two different contacts, FounderHQ
answers 409 and sends nothing.
Content for each channel
| Field | Required | Notes |
|---|---|---|
subject | Yes | |
html | Yes | |
text | No | The plain-text version |
fromEmail | No | Must be on a domain your connection has verified, or FounderHQ answers 422. Default: the connection's sender |
fromName | No | |
replyTo | No | |
headers | No | Up to 20 custom headers. Not To, Cc, Bcc, From, Reply-To, Subject, or other headers FounderHQ sets |
SMS
| Field | Required | Notes |
|---|---|---|
text | Yes | |
senderId, dltTemplateId, smsType | No | For India. See India DLT |
Push
| Field | Required | Notes |
|---|---|---|
title | Yes | |
body | Yes | |
link | No | A URL or deep link the app opens on tap |
imageUrl | No | |
notificationKey | No | The name of the notification on the device, such as order-1042. A later push with the same key replaces it, and a removal names it. What it renders to is 64 bytes at most. With none, the message ID is used. See Replace or remove a notification |
data | No | Up to 50 string values for your app |
WhatsApp accepts free text only within 24 hours of the contact's last message to you. At any other time, send a template that WhatsApp approved.
| Field | Required | Notes |
|---|---|---|
templateName | One of the two | The approved template's name in WhatsApp |
languageCode | No | Default en_US |
headerParameters, bodyParameters | No | Lists of { "value": "..." }, in the template's order. Add "name" for a named parameter |
buttonParameters | No | Lists of { "index": "0", "parameters": [{ "value": "..." }] } |
text | One of the two | Free text, inside the 24-hour window |
{
"channel": "whatsapp",
"to": { "phone": "+12125550123" },
"content": {
"templateName": "order_shipped",
"languageCode": "en",
"bodyParameters": [{ "value": "Jane" }, { "value": "1042" }]
}
}This works on a Meta WhatsApp connection. On a Twilio connection, a
template with parameters must be imported in FounderHQ first: send it with
template, not with content.
Remove a push notification
POST https://app.getfounderhq.com/api/v1/push/remove takes a notification
off a person's phones and tablets. Use the same secret key.
Name the notification in one of two ways:
{ "messageId": "9f0c1c0e-6a51-4a6e-9a7f-2b9d6f0f1a11" }{ "to": { "externalId": "user_42" }, "notificationKey": "order-1042" }| Field | Notes |
|---|---|
messageId | The id of a push this API sent. Its person and its notification key are used |
to | { "contactId": "..." } or { "externalId": "..." }. The contact must exist; none is created |
notificationKey | With to. The key the push was sent with, 64 bytes at most |
The answer says how many devices were asked:
{ "notificationKey": "order-1042", "devices": 2, "accepted": 2, "failed": 0 }accepted counts the devices whose push service took the request. It does
not mean the notification is gone: the app removes it when the silent push
arrives. A person with no app device answers with devices: 0. Browsers are
not counted; a page removes its own notification. Your app needs the SDK
setup under
Remove from your server.
Personalize the content
Every text field in content is a Liquid template, the same as a template
you build in FounderHQ. Use {{ contact.firstName }}, {{ contact.email }},
{{ brand.name }}, and {{ vars.name }} for a value from variables.
FounderHQ refuses the request with 422 when the content uses a variable
that has no value. Add a default to make one optional:
{{ contact.firstName | default: "there" }}.
If your own text contains {{, write it as {{ "{{" }}. FounderHQ then
sends the two braces as they are.
FounderHQ refuses content that takes more than 2 seconds to render, or that renders to more than 2 MB.
Choose the connection
Leave connection out and FounderHQ sends through the brand's default
connection for the channel. Push goes through every push connection.
To send through a specific one, pass its id. In FounderHQ, open Sequences settings, open the connection's menu, and choose Copy ID. Or list the active connections of your key's brand:
curl "https://app.getfounderhq.com/api/v1/connections?channel=email" \
-H "Authorization: Bearer fhq_sk_XXXX"{
"connections": [
{
"id": "7a1c9d2e-4b3f-4e8a-b6c5-2d1e0f9a8b7c",
"name": "Acme transactional",
"channel": "email",
"provider": "resend",
"isDefault": true
}
]
}For each channel, the first connection in the list is the one FounderHQ
uses when you leave connection out.
Transactional and marketing
A message is transactional unless you send "category": "marketing".
- Transactional: for messages the contact expects, like a receipt. FounderHQ sends it at once, also to a contact who unsubscribed from marketing email.
- Marketing: FounderHQ checks that the contact did not unsubscribe, adds the unsubscribe header to email, and holds the message during your quiet hours.
Both skip an address that bounced or complained, and a contact who opted out of SMS or WhatsApp.
Add an unsubscribe link to a marketing email with
{{ system.unsubscribeUrl }}.
Send safely more than once
Send an Idempotency-Key header with a value that is unique to the message,
such as receipt-order-1042. If your server retries the request with the
same key, FounderHQ answers 200 with the first message and sends nothing
new.
A key is remembered for as long as the message is in your log, and FounderHQ does not compare the two requests. Use a new key for every new message.
Verify
Ask for the message by its id:
curl https://app.getfounderhq.com/api/v1/messages/MESSAGE_ID \
-H "Authorization: Bearer fhq_sk_XXXX"status moves from queued to sent, then to delivered when the provider
confirms it. In FounderHQ, open Sequences → Message log. The message is in the
log with the source API.
| Status | Meaning |
|---|---|
queued, sending | On its way |
sent | The provider accepted it |
delivered | The provider confirmed delivery |
bounced | The address refused it |
failed | FounderHQ could not send it. error says why |
suppressed | Not sent: the contact unsubscribed, or the address is blocked. error says which |
skipped | Not sent: the contact was deleted before FounderHQ sent it |
For a push, to is null: the message goes to the contact's devices.
Troubleshoot
403 "API key type is not allowed here"
You sent a publishable key (fhq_pk_). Use a secret key (fhq_sk_).
401 or 403 with a secret key
The key cannot send messages. Create a key with Send messages from your server. That key can also create and update contacts, which a send needs.
422 "No active email connection"
The brand of your key has no active connection for this channel. Connect one, or finish its setup, in Sequences settings.
422 "Publish this template before sending it."
The template has no published version. Open it in FounderHQ and publish it.
422 "This person has no device that can get a push."
The contact has not registered a device. See Register a device.
400 "Attachments are not supported yet."
The API does not send attachments. Put a link to the file in the message.