# Custom HTTP integration (https://www.getfounderhq.com/docs/sequences/custom-http-integration)

Send through any provider with an HTTP API, by describing the request
FounderHQ should make. About 20 minutes with the provider's API docs open.

<Callout type="info" title="Before you start">
Check [Sequences providers](/sequences) first — if your
provider already ships as a recipe, use that instead and skip this page.
You need the provider's send-endpoint documentation and an API credential.
Then open Sequences in FounderHQ and add an HTTP integration.
</Callout>

## How it works

You define one HTTP integration: a request template plus how to read the
response. You then create a channel connection that points at it and picks
the channel — email, SMS, WhatsApp, or push.

For every message the sequence sends, FounderHQ fills your template with
that message's data, calls your endpoint, and reads the reply to learn
whether it worked.

## Build the request

**Method** — GET, POST, PUT, PATCH, or DELETE.

**URL** — must be a public `http` or `https` address. It can contain
variables.

**Headers and query parameters** — plain key/value pairs. Values can
contain variables.

**Body** — pick one of three modes:

| Mode | What you write | When to use it |
| --- | --- | --- |
| `json` | A JSON structure. Variables run inside string values. | Most JSON APIs |
| `form` | Field names and values. Each value is URL-encoded after it renders. | Form-encoded APIs. You never need escaping filters. |
| `raw` | One string, sent exactly as it renders. | Anything else, or when you need a conditional around whole branches of JSON |

Templating uses Liquid. The `include`, `render`, and `layout` tags are
turned off.

```
{ "to": {{ recipient.phone | json }},
  "text": {{ message.text | json }},
  "callback": {{ channel.statusCallbackUrl | json }} }
```

The `json` filter is worth using on every value in `raw` mode. It quotes
and escapes the value, so an apostrophe in a message body cannot break your
JSON.

A variable with no value renders as an empty string rather than failing —
with one exception, described under [Guards](#guards).

## Variables you can use

**The person**

| Variable | Value |
| --- | --- |
| `recipient.value` | Whatever address this channel sends to |
| `recipient.email` | Email channel only |
| `recipient.phone` | SMS and WhatsApp channels |
| `recipient.token` | Push channel — the device token |
| `recipient.externalId` | Your own ID for the contact, when the provider resolves people itself |

**The message**

| Variable | Value |
| --- | --- |
| `message.outboundMessageId` | FounderHQ's ID for this send. Pass it through if the provider echoes a reference back. |
| `message.contactId` | The contact this send belongs to |
| `message.subject` | Email subject |
| `message.text` | Plain-text body |
| `message.html` | HTML body |
| `message.title` | Push title |
| `message.body` | Push body |
| `message.templateName` | The approved template name, on template sends |
| `message.senderId` | India DLT sender header, from the SMS template |
| `message.dltTemplateId` | India DLT template ID, from the SMS template |
| `message.smsType` | India DLT category |
| `message.payload` | The whole rendered payload, for anything not listed above |

**The connection**

| Variable | Value |
| --- | --- |
| `channel.type` | EMAIL, SMS, WHATSAPP, or PUSH |
| `channel.providerType` | The provider kind behind this connection |
| `channel.connectionId` | This connection's ID |
| `channel.statusCallbackUrl` | A delivery-report address unique to this connection, with its own token |

**WhatsApp template sends** also get `message.metaTemplate` (the whole
Cloud API template object, for providers that pass Meta's format through),
`message.bodyParamTexts` (the body variables in order), and
`message.bodyParamMap` (the same values keyed by name or position).

## Authenticate

You store exactly one secret per integration. Pick how it is sent:

| Type | What FounderHQ sends |
| --- | --- |
| `none` | Nothing |
| `bearer` | `Authorization: Bearer <secret>` |
| `basic` | `Authorization: Basic <base64 of username:secret>` |
| `custom_header` | A header you name, with an optional literal prefix before the secret — for example `Basic ` for a key you already encoded |
| `body_field` | The secret as a top-level field of the JSON body, under a name you choose |
| `hmac_sha256` | An HMAC-SHA256 of the request body, signed with the secret, in a header you name. Defaults to `x-founderhq-signature` and a `sha256=` prefix. |

The secret is applied after the template renders, so it never appears in
anything FounderHQ stores or shows you.

## Read the response

Point FounderHQ at the fields it should read, using dotted paths like
`data.id` or `messages.0.id`.

| Setting | What it finds |
| --- | --- |
| `idPath` | The provider's message ID. Delivery reports match against it. Falls back to a top-level `id`. |
| `urlPath` | A link to the message at the provider. Falls back to a top-level `url`. |
| `statusPath` | A status value in the body. Falls back to a top-level `status`. |
| `errorPath` | The error text to show you when the send fails. Falls back to `error`, then `message`. |
| `failStatus` | A status value that means failure even on an HTTP 200. Set this for APIs that answer 200 with `{"type":"error"}`. |

FounderHQ retries on 429 and on 5xx responses. Every other failure is
permanent — a bad key or a rejected recipient stops rather than looping.
Requests time out after 15 seconds.

## Delivery reports

`channel.statusCallbackUrl` is a per-connection address carrying its own
token. Two ways to use it:

- Put it in the send request, if the provider takes a callback URL per
  message. Nothing to configure at the provider.
- Paste it into the provider's dashboard, if that is where webhooks are
  set.

FounderHQ understands the Meta WhatsApp envelope and the delivery-receipt
formats of the SMS providers it ships recipes for. A provider with a shape
of its own can POST FounderHQ's normalized status format instead.

If you wire no callback at all the integration still works. Messages just
stop at Sent, and sequence steps cannot wait on delivery.

## Guards

FounderHQ refuses to send a request it knows is wrong:

- **No recipient.** Your template must reference the right recipient
  variable for the channel, and it must have a value. Otherwise the send
  fails instead of going out addressed to nobody.
- **Empty regulatory fields.** If your template references an India DLT
  variable, that variable must have a value. It is never sent blank. See
  [India DLT](/sequences/india-dlt).
- **Session messages the provider cannot take.** A connection marked as
  template-only rejects free-text WhatsApp sends outright.

## Verify

1. Use the integration's test send. It shows the exact request FounderHQ
   built and the provider's raw reply.
2. The reply's message ID appears in the mapped result. If it reads empty,
   fix `idPath`.
3. Build a sequence with one step on this connection and run yourself
   through it. The message arrives, and the sequence shows it as Sent.

## Troubleshoot

### The test send says a required variable is missing

Your template references something the test context has no value for.
Check the variable names against the tables above.

### The provider answers 200 but nothing arrives

The API is reporting failure inside a successful response. Set `statusPath`
and `failStatus` so FounderHQ treats it as a failure too.

### Messages send but never reach Delivered

No callback is wired. Either add `channel.statusCallbackUrl` to the send
request, or paste it into the provider's webhook settings.

### The body is malformed when a message contains quotes

You are in `raw` mode without the `json` filter. Write
`{{ message.text | json }}`, not `"{{ message.text }}"`.
