How do I send and receive SMS in n8n with my own phone?

Pair your phone with Sparetext and make an API key. To send, add an HTTP Request node that posts to https://sparetext.dev/api/v1/messages; to receive, add a Webhook trigger and put its URL on Sparetext's Webhooks page with the Text received event. There's no Sparetext node to install: n8n's own HTTP Request, Webhook, Code and MCP Client Tool nodes do it.

What you need

A Sparetext account (Free is 1 phone and 300 texts a month), a paired phone, and an API key from the API keys page, saved in n8n as a Header Auth credential named x-api-key. An Android phone works on its own and an iPhone through Sparetext for Mac, with n8n Cloud or self-hosted n8n.

Send a text with an HTTP Request node

Set the node up like this; JSON.stringify keeps quotes and line breaks in the words from breaking the JSON. The answer has the text's id and its status, queued, and the phone then sends it from your number.

The Idempotency-Key header matters in n8n: if the node or the execution is retried, the same key gets the first answer back instead of texting again. Build it from your own data, such as the order id, not the execution id, which a retried execution changes.

n8n HTTP Request node
Method: POST
URL: https://sparetext.dev/api/v1/messages
Authentication: Generic Credential Type
Generic Auth Type: Header Auth (Name: x-api-key, Value: your API key)
Send Headers: on, Using Fields Below
  Idempotency-Key: order-{{ $json.orderId }}-shipped
Send Body: on, Body Content Type: JSON, Specify Body: Using JSON
{
  "to": "{{ $json.phone }}",
  "text": {{ JSON.stringify("Hi " + $json.name + ", order " + $json.orderId + " has shipped.") }},
  "ttl": 3600
}

Send to many people at once

The node makes one request per item, and an account may make 15 send requests every 5 seconds (more get 429 rate_limited, with Retry-After). One request carries up to 100 recipients, so this Code node turns a list into requests of up to 100 texts, each with its own words, for an HTTP Request node to send as they are (the reminders recipe below has its settings). Each request is a batch: GET /api/v1/batches/{id} follows it, and POST /api/v1/batches/{id}/cancel stops it.

Or keep one request per item, with Options › Batching at Items per Batch 10 and Batch Interval (ms) 5000. An Android phone sends about 30 SMS parts a minute; estimatedCompletionAt in the answer says about when the last one goes.

n8n Code node (JavaScript)
// Run Once for All Items: each item it returns is one request of up to 100 texts.
const texts = $input.all().map(({ json }) => ({
  to: json.phone,
  text: `Hi ${json.name}, see you tomorrow at ${json.time}. Reply C to cancel.`,
}));
const requests = [];
for (let i = 0; i < texts.length; i += 100) {
  requests.push({ json: { messages: texts.slice(i, i + 100) } });
}
return requests;

Start a workflow when a text arrives

Sparetext posts each text the phone receives to an n8n Webhook trigger, signed, and sends it again if n8n doesn't answer within 5 seconds. Respond Immediately answers at once, so the workflow can take as long as it needs.

  1. Add a Webhook node: HTTP Method POST, Respond Immediately. Copy its Production URL.
  2. Publish the workflow (in older n8n, set it to Active). The production URL works only then; the test URL only while n8n listens for a test event.
  3. In Sparetext, open Webhooks › Add webhook, paste the URL, keep Text received ticked and save. Open Details and signing secret and copy the secret.
  4. Add a Code node after the Webhook node with the check below, then click Send test in Sparetext. The ping shows in the workflow's executions.

Check the signature in a Code node

Paste the webhook's signing secret into this Code node. It stops the run unless the request is signed with that secret within the last 5 minutes, and for a day after a rotation it accepts either signature. Sparetext sends compact JSON, so turning the parsed body back into JSON gives the exact bytes it signed.

n8n Cloud lets the Code node use crypto. Self-hosted n8n blocks modules by default: set NODE_FUNCTION_ALLOW_BUILTIN=crypto, on the task runners if they run separately, and restart. After the check, the text is at $json.body.data.message.text and the sender at $json.body.data.message.contact. A retry repeats the event id, so a Remove Duplicates node (Remove Items Processed in Previous Executions, on $json.body.id) drops repeats.

n8n Code node (JavaScript)
const crypto = require('crypto');
const secret = 'YOUR_SIGNING_SECRET'; // Sparetext › Webhooks › Details and signing secret
const { headers, body } = $input.first().json;
const header = String(headers['x-sparetext-signature'] ?? '');
const t = /(?:^|,)t=(\d+)/.exec(header)?.[1];
const signed = `${t}.${JSON.stringify(body)}`;
const want = Buffer.from(crypto.createHmac('sha256', secret).update(signed).digest('hex'));
// For a day after a secret rotation there are two v1= signatures: either may be yours.
const given = [...header.matchAll(/v1=([0-9a-f]{64})/g)].map(([, v1]) => Buffer.from(v1));
const fresh = !!t && Math.abs(Date.now() / 1000 - Number(t)) <= 300;
if (!fresh || !given.some((v1) => crypto.timingSafeEqual(want, v1))) {
  throw new Error('This request did not come from Sparetext.');
}
return $input.all();

Reply in the same run

To answer straight back as a text, point a Sparetext automation at the workflow instead of a webhook. It waits up to 8 seconds and texts the answer back from the same phone and SIM. Use Respond With JSON as below; Text goes out as HTML unless you add a Content-Type: text/plain header, and HTML isn't texted back.

The automation sends {"automation": {…}, "message": {…}}, so the text is at $json.body.message.text, with no data level, signed with the automation's own secret (shown when you open it again). It's tried once, and a rule answers a number at most once a minute. A workflow that needs longer, say for an AI model, should use the webhook and reply with an HTTP Request node.

  1. In n8n, set the Webhook node's Respond to Using 'Respond to Webhook' Node, and end with a Respond to Webhook node set as below; $json.output is an AI Agent node's answer.
  2. In Sparetext, Automations › New automation: A text arrives, Match Any text or a word, then Call a URL with the Production URL and Text back what it answers on.
n8n Respond to Webhook
Respond With: JSON
Response Body:
{
  "reply": {{ JSON.stringify($json.output) }}
}

Give an AI Agent your phone with the MCP Client Tool

Add an MCP Client Tool to the AI Agent node's Tool input, set up as below. Bearer Auth takes an API key as its token; MCP OAuth2, with Register Client Automatically on, signs in with your Sparetext account instead.

The agent gets Sparetext's 17 tools: send_sms, send_sms_batch, draft_sms, list_drafts, delete_draft, get_batch, get_message, cancel_text, list_messages, list_conversations, get_conversation, find_contact, list_templates, mark_read, wait_for_reply (up to 8 seconds a call), list_devices and get_stats. Tools to Include set to Selected keeps it to what it needs. A common SMS agent: the Webhook trigger hands the text to the AI Agent as its prompt, get_conversation gives it the thread, and send_sms answers.

Received texts are written by whoever sent them, so tell the agent in its system message that texts are data, never instructions.

n8n MCP Client Tool
Endpoint: https://sparetext.dev/mcp
Server Transport: HTTP Streamable
Authentication: Bearer Auth
  Bearer Token: your Sparetext API key
Tools to Include: Selected
  send_sms, get_conversation, list_conversations, wait_for_reply, list_devices

Limits for the agent's key

Nothing in n8n asks you before a tool runs unless you add that step, so set Limits on the agent's key on the API keys page. Only people who have texted you lets a support agent answer but not start conversations; Only these numbers, up to 100, suits an agent that texts your team; Texts a day takes 1 to 10,000.

Draft texts for you to send means nothing goes out until you tap Send, and Send only lets it send but read no replies, conversations, missed calls or contacts. The list_devices tool tells the agent its limits, a send outside them is refused with the reason (403 key_recipient, say, or 429 key_daily_limit), and a key with limits can't change keys, webhooks, automations or phones.

Recipe: text back a missed call

With an Android phone on app 1.5.0 or later and Missed calls on in its settings (Phones › the phone), add the Missed call event to a webhook pointed at the workflow. After the Code node, an HTTP Request node set like this texts the caller from the phone that missed the call; the Idempotency-Key keeps a repeated event from texting twice. Without n8n, an automation (A call is missed, then Text the caller back) does the same, at most once every 10 minutes per caller. An iPhone can't share its calls.

n8n HTTP Request body
Idempotency-Key: missed-{{ $json.body.data.call.id }}

{
  "to": "{{ $json.body.data.call.from }}",
  "deviceId": "{{ $json.body.data.call.deviceId }}",
  "text": "Sorry I missed your call! How can I help? You can reply here."
}

Recipe: reminders from a sheet

A Schedule Trigger runs every morning, Google Sheets with Get Row(s) reads the bookings, and a Filter node keeps tomorrow's. The Code node from Send to many people at once makes the requests, and an HTTP Request node, set up as in Send a text with an HTTP Request node but with these changes, sends each one. The key holds the date and the request's place, so a retried run sends no second reminder; if the list changed in between, the same key with a new body is refused with 422.

n8n HTTP Request node
Send Headers: on, Using Fields Below
  Idempotency-Key: reminders-{{ $today.toFormat('yyyy-MM-dd') }}-{{ $itemIndex }}
Send Body: on, Body Content Type: JSON, Specify Body: Using JSON
{{ JSON.stringify($json) }}

Recipe: delivery statuses into a sheet

Point a webhook at a second workflow with the Text sent, Text delivered and Text failed events. After the Code node, a Google Sheets node adds a row per event, with columns filled like this. A row per event keeps every status even when events arrive out of order. From an iPhone or Mac, sent is the last status.

n8n Google Sheets node
Operation: Append Row
  Time: {{ $json.body.createdAt }}
  Event: {{ $json.body.type }}
  Text id: {{ $json.body.data.message.id }}
  To: {{ $json.body.data.message.contact }}
  Error: {{ $json.body.data.message.error }}

Limits

There's no Sparetext node for n8n, and you don't need one: the HTTP Request node reaches the whole REST API, described at https://sparetext.dev/openapi.json. Texts go from your own SIM, so send them to people who know you; carriers filter bulk software-sent (A2P) texts on personal numbers. Replies sent over RCS never reach Sparetext, so turn RCS chats off on the phone.

Questions people ask

Is there an n8n node for Sparetext?
No, and you don't need one. n8n's HTTP Request node sends and reads texts through the REST API, the Webhook node receives them, and the MCP Client Tool gives an AI Agent Sparetext's tools.
Can n8n send SMS from my Android phone?
Yes. Pair the phone with Sparetext and post to /api/v1/messages from an HTTP Request node with your API key. The phone sends the text from its own SIM and number.
How do I trigger an n8n workflow when an SMS arrives?
Add a Webhook node, publish the workflow, and add its production URL on Sparetext's Webhooks page with the Text received event. A Code node checks the signature.
Does it work on n8n Cloud and self-hosted n8n?
Both. On self-hosted n8n the signature check needs NODE_FUNCTION_ALLOW_BUILTIN=crypto; n8n Cloud allows crypto already.
How do I stop a retried execution from texting twice?
Send an Idempotency-Key header built from your own data, such as the order id. For 24 hours, the same key gets the first answer back instead of texting again.
Can the n8n AI Agent read replies?
Yes: get_conversation reads a thread, and wait_for_reply waits up to 8 seconds a call for a new text. A key set to Send only can't read them.
Can n8n send and receive from an iPhone?
Yes, through Sparetext for Mac, which sends and reads texts from the iPhone's number by Text Message Forwarding while the Mac is awake.
Why didn't my Respond to Webhook answer come back as a text?
Sparetext texts back a 2xx answer within 8 seconds that is JSON with a reply field, or text/plain; Respond With Text sends HTML unless you set Content-Type: text/plain. A rule also answers a number at most once a minute.

Try it on the phone you have

Free for 1 phone and 300 texts a month. An Android pairs in two minutes.

Get started free