n8n
Self-hosted or cloud n8n workflows that send iMessage and process replies, including proper signature verification in a Code node.
n8n is the best of the no-code options for this, because you can verify the webhook signature and still keep the build visual.
Credential setup
Credentials, New, Header Auth. Name it Blue Reacher. Header name Authorization, header value Bearer brk_your_api_key. Every HTTP Request node then references the credential instead of carrying the key.
Outbound workflow
- Your trigger node: Schedule, Webhook, a database node, a CRM node.
- HTTP Request node:
- Method POST
- URL
https://api.bluereacher.com/v1/messages - Authentication: Generic Credential Type, Header Auth, Blue Reacher
- Send Body: on, Body Content Type JSON
- Specify Body: Using JSON
{
"to": "{{ $json.phone }}",
"message": "Hi {{ $json.first_name }}, it's Marcus from Northside. Quick question about your enquiry.",
"metadata": { "crm_id": "{{ $json.id }}" }
}- Add Settings, Retry On Fail with 3 tries for transient
5xxresponses. Do not retry4xx; those need a fix, not a repeat. Error semantics are in errors and status codes.
Pacing a batch
Sends are paced platform-side, so you do not need to build delays for line health, and cold volume past today's capacity queues rather than erroring. The one limit your workflow can hit is the API request rate (30/min on POST /v1/messages): a Split In Batches node with a small Wait keeps a big batch under it, and available_today from GET /v1/devices at the start of the run tells you how much lands today versus queues for tomorrow.
Inbound workflow with signature verification
- Webhook node, method POST. Set Response Mode to Immediately so n8n acknowledges before processing.
- Register the production URL as your Blue Reacher webhook endpoint.
- Code node, run once for each item:
const crypto = require('crypto');
const raw = JSON.stringify($input.item.json.body);
const provided = $input.item.json.headers['x-bluereacher-signature'];
const expected = 'sha256=' + crypto
.createHmac('sha256', $env.BLUEREACHER_WEBHOOK_SECRET)
.update(raw)
.digest('hex');
if (
!provided ||
provided.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(expected))
) {
throw new Error('Invalid signature');
}
return $input.item;Enable Raw Body on the Webhook node if your n8n version re-serializes JSON differently from the sent bytes, and hash the raw string instead. A signature that never matches is almost always this.
- Switch node on
{{ $json.body.event }}: routemessage.receivedto your CRM update,contact.opted_outto your suppression flag,message.sent/message.failedto your delivery logging.
A pattern worth building
Reply, then classify, then route. Send the inbound text to an LLM node with a short prompt that returns one of interested, not-interested, question, or wrong-number, then branch: interested pings the rep in Slack immediately, question drafts a suggested answer for a human to approve, not-interested and wrong-number update the CRM and stop the sequence.
Keep a human in the loop on anything that sends. Auto-replying to a live prospect from a classifier is how a good conversation becomes a bad one, and an incorrect auto-reply to someone asking to opt out is a compliance problem, not just an awkward one.

