# Send a message from your server (https://www.getfounderhq.com/docs/sequences/send-api)

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.

<Callout type="info" title="Before you start">
You need an active connection for the channel: [email](/sequences/email),
[SMS](/sequences/sms), [WhatsApp](/sequences/whatsapp), or
[push](/sequences/push). You also need a secret key that can send messages.
In FounderHQ, open [Settings → API Keys](https://app.getfounderhq.com/?openSettings=api-keys), choose **Send messages from your
server**, and store the key in your server environment.
</Callout>

## Send with the content in the request

Put the whole message in `content`. You do not need a template.

```bash
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.

```json
{
  "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.

```bash
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:

```bash
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](#who-gets-the-message) |
| `content` | One of the two | The message itself. See [Content for each channel](#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](#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. An `email` or `phone`
  the contact did not have is saved on the contact.
- `contactId` alone: 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](/sequences/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

**Email**

| 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](/sequences/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](/sequences/push#replace-or-remove-a-notification) |
| `data` | No | Up to 50 string values for your app |

**WhatsApp**

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 |

```json
{
  "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:

```json
{ "messageId": "9f0c1c0e-6a51-4a6e-9a7f-2b9d6f0f1a11" }
```

```json
{ "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:

```json
{ "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](/sequences/push#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:

```bash
curl "https://app.getfounderhq.com/api/v1/connections?channel=email" \
  -H "Authorization: Bearer fhq_sk_XXXX"
```

```json
{
  "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:

```bash
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](https://app.getfounderhq.com/sequences/messages). 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](/sequences/push#register-a-device).

### `400` "Attachments are not supported yet."

The API does not send attachments. Put a link to the file in the message.
