For developers
An iMessage API built for replies, not just deliveries
Beam sends and receives business iMessage through a workspace API, with RCS where supported and SMS fallback. Send a server-side request, verify signed reply events, and check the recorded delivery status separately from queue acceptance.
Beam is not affiliated with Apple. iMessage is a trademark of Apple Inc. Beam sends from dedicated business numbers, not from a personal Apple ID.
Quick answer
Beam provides an iMessage API for business workspaces, with RCS where supported and SMS fallback. Use its API, signed event webhooks and MCP access to connect messaging with your applications and authorized AI agents.
Follow the iMessage API integration guides to test sends, replies and agent handoffs, and the RCS and SMS fallback guides to check recipient channels.
What you are actually calling
Every /v1/* request carries your workspace key in an x-api-key header. Keep it server side.
Requests and responses are JSON. Errors come back as a plain sentence in an error field with
a matching HTTP status. Phone numbers are accepted as US 10 digit, 11 digit or full
international format, and Beam normalizes them.
The whole surface is small enough to read in one sitting:
| Method and path | What it does |
|---|---|
POST /v1/messages |
Send a message |
GET /v1/messages/:id |
Get a message and its delivery status |
DELETE /v1/messages/:id |
Cancel a queued message |
GET /v1/messages/list |
List recent messages |
GET /v1/conversations/:phone |
Full history with one contact |
POST /v1/reactions |
Land a tapback on a contact's message |
POST /v1/typing |
Show the typing indicator |
POST /v1/read |
Send a read receipt |
GET /v1/contacts/:phone |
Contact details and opt-out state |
GET /v1/availability/:phone |
Blue bubble support check |
GET /v1/numbers |
The workspace's lines and their warm-up caps |
POST /t/:workspace/optin |
New lead webhook for CRM workflows |
The OpenAPI file is the contract. Messaging, presence, numbers, the CRM opt-in webhook and email are all in it. Read the API introduction for the short map, or open the OpenAPI contract for the real thing.
One call, three channels
Beam checks whether the recipient's phone supports blue bubbles, then chooses:
- iMessage for eligible iPhones on a capable line.
- RCS when the Android recipient's phone and carrier support it.
- SMS fallback for other recipients your assigned line can reach.
Beam sends from Apple and Android devices. The first number a contact hears from stays theirs, so their whole relationship with your business lives in one thread on their phone.
Three rules worth writing on a wall before you build:
- Queued is not delivered. A 200 means the send was accepted into a paced queue. Poll
GET /v1/messages/:idor subscribe to webhooks for the real status.sentis notdelivered. - Uncertain stays uncertain. Beam does not silently re-send a blue bubble message or switch it to SMS after an ambiguous provider update. SMS fallback after a confirmed failed iMessage is a workspace setting.
- Opt-outs are enforced at the send layer. A send to a contact who opted out returns 403 and nothing goes out.
Statuses: queued, sent, delivered, failed, cancelled, no_channel.
Registration note: iMessage and FaceTime Audio run outside carrier A2P 10DLC. SMS fallback remains subject to applicable carrier A2P requirements.
Voice runs on the same contact. Beam FaceTime Audio connects directly to Voice AI agents, and an inbound call on a Beam line routes to your team or to that agent. Beam uses the caller's phone number as the shared identity, so a lead who calls and a lead who texts are one conversation and one CRM contact rather than two dead ends.
| Recipient and line | Route to evaluate | Evidence to retain |
|---|---|---|
| Eligible iPhone and capable line | iMessage | Recorded channel, delivery state and a phone reply |
| Android phone and carrier support RCS | RCS | Actual send channel and test-phone receipt |
| Other reachable recipient | SMS fallback | Line eligibility and recorded SMS result |
| Ambiguous provider update | No automatic resend or downgrade | Reconcile the original record before intentional follow-up |
Prove one send, one signed reply and the recorded status
Use an active workspace, an assigned line and one consented test phone you control. These examples follow Send a message, Webhook signing and Event webhooks. They are a test procedure and illustrative fixtures, not a captured customer exchange.
- Configure an HTTPS receiver in Settings, Event webhooks. Store the signing secret on your server. It is separate from the API key.
- Run the send request below after replacing both placeholders. Save the returned message ID.
- Read that ID with the status request. Observe the receiving phone; an accepted request or message.sent event is not a delivery receipt.
- Reply from the test phone and verify the signature before parsing or acting on the event. Correlate the reply by workspace and its from/to phone pair.
- Record the actual channel and status. Repeat with an Android test phone to observe RCS or SMS eligibility for your assigned line. Do not claim a fallback route from a capability lookup alone.
curl --fail-with-body -X POST https://beam.aisync.link/v1/messages \
-H "x-api-key: YOUR_WORKSPACE_KEY" \
-H "Content-Type: application/json" \
-d '{"to":"YOUR_TEST_PHONE_E164","message":"Integration test: please reply Tuesday."}'
curl --fail-with-body https://beam.aisync.link/v1/messages/RETURNED_MESSAGE_ID \
-H "x-api-key: YOUR_WORKSPACE_KEY"
The documented send response has this shape. The ID is illustrative; use the real returned value in your status request.
{"id":"41","status":"queued","to":"YOUR_TEST_PHONE_E164"}
Verify the exact raw bytes in Node.js
Save as verify-beam.mjs. Pass a raw Buffer from your HTTP receiver and the Beam-Signature header. Run verification before JSON middleware changes the body. This rejects malformed signatures and timestamps outside a five-minute window in either direction.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyBeamSignature(rawBody, header, secret, now = Date.now()) {
if (!secret || !Buffer.isBuffer(rawBody) || typeof header !== "string") return false;
const parts = header.split(",");
if (parts.length !== 2 || !parts[0].startsWith("t=") || !parts[1].startsWith("v1=")) return false;
const t = parts[0].slice(2);
const hex = parts[1].slice(3);
if (!t.length || [...t].some(c => c < "0" || c > "9")) return false;
if (hex.length !== 64 || [...hex].some(c => !"0123456789abcdef".includes(c))) return false;
const seconds = Number(t);
if (!Number.isSafeInteger(seconds) || Math.abs(now / 1000 - seconds) > 300) return false;
const expected = createHmac("sha256", secret)
.update(t + ".").update(rawBody).digest();
const supplied = Buffer.from(hex, "hex");
return supplied.length === expected.length && timingSafeEqual(expected, supplied);
}
Recognize a signed reply
This illustrative payload follows the event guide's id/type/timestamp/data envelope. The public OpenAPI webhook schema currently lists event/created_at/data instead. Confirm the envelope on your own signed test delivery before connecting production automation; the example below uses the event guide, not a claim that the two schemas are identical.
{
"id": "evt_example_reply",
"type": "message.received",
"timestamp": 1787340000,
"data": {
"from": "YOUR_TEST_PHONE_E164",
"to": "YOUR_ASSIGNED_LINE_E164",
"body": "Tuesday",
"channel": "imessage",
"attachments": []
}
}
The timestamp above is a fixture, not a fresh signed delivery. Do not paste it into a live verification test. Generate a current signed fixture locally or receive a real reply from your authorized test phone. The inbound payload does not promise the outbound message ID: keep your own workspace/contact correlation.
After verification, deduplicate by workspace and event ID, durably store or enqueue the event, and return a 2xx promptly. Do not treat an in-memory deduplication set as durable storage. Beam documents one attempt, a five-second timeout and no automatic event retries. Reconcile missed events with conversation history and message listing.
Acceptance, delivery and replies are separate evidence
| Observation | What it proves | What to do next |
|---|---|---|
| HTTP 200 and queued | Beam accepted the send into its queue | Save the ID and inspect GET /v1/messages/:id |
| message.sent | An outbound message went out | Do not label it delivered solely from this event |
| Recorded delivered status | The message record reports delivery | Compare with the test phone; do not infer a read or reply |
| Verified message.received | A reply event passed signature checks | Match the workspace and phone pair before updating the conversation |
| Failed, no_channel or uncertain result | The desired outcome is not established | Inspect history before another send; do not blindly replay POST /v1/messages |
The public contract does not document a request_key for the v1 message-send body. Do not transfer the workflow endpoint's duplicate protection to this endpoint. A timeout calls for reconciliation before another send.
Keep reseller tenants separate
Map your internal tenant ID to its Beam workspace key and webhook secret on the server. Select that mapping from an authenticated account or a workspace-specific receiver route, never from a tenant value in an unverified body. Store status and event records under the workspace as well as the contact. A second client's identical phone number must not expose the first client's history.
Before handoff, use the business texting app checklist to confirm who owns replies and the business messaging use cases to choose one pilot workflow.
From key to first message
- Get your workspace key. Sign in and open Settings. API keys are for technical integrations. Keep them server side, never in a browser or a mobile app.
- Check the number.
GET /v1/availability/:phonetells you whether a phone supports blue bubbles before you spend a send. - Send.
POST /v1/messageswithtoandmessage. - Read the status.
GET /v1/messages/:id, or better, take the signed webhook. - Watch the inbox. The conversation appears as soon as the message sends, and the reply lands right under it.
Full walkthrough: Quickstart.
Send an iMessage programmatically
Send a message (curl).
curl -X POST https://beam.aisync.link/v1/messages \
-H "x-api-key: YOUR_WORKSPACE_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "RECIPIENT_E164",
"message": "Thursday at 10:00 or 2:30. Which works better?"
}'
Response:
{
"id": "41",
"status": "queued",
"to": "RECIPIENT_E164"
}
Optional fields on the same endpoint: first_name, attachments (public HTTPS links, up to 10
on blue bubbles and 3 on texts) and effect (a full screen effect that applies on blue bubbles
and is ignored on texts). Cancel a message that is still queued with
DELETE /v1/messages/:id. Reference: Send a message.
Check blue bubble support before you send (curl).
curl "https://beam.aisync.link/v1/availability/RECIPIENT_E164" \
-H "x-api-key: YOUR_WORKSPACE_KEY"
Response:
{ "phone": "RECIPIENT_E164", "imessage": true }
null means the check could not complete. Treat that as unfinished, not as proof the phone is
text only. For a list, screening accepts up to 25,000 numbers per check and reuses recent
results for 24 hours. Screening does not deliver a message and does not notify the person.
Reference: Check a list.
Send from Node (fetch).
const res = await fetch("https://beam.aisync.link/v1/messages", {
method: "POST",
headers: {
"x-api-key": process.env.BEAM_KEY,
"Content-Type": "application/json",
},
body: JSON.stringify({
to: process.env.RECIPIENT_E164,
message: "Thanks for reaching out. What made you start looking?",
}),
});
const data = await res.json();
// { id: "41", status: "queued", to: "..." }
Signed events, not polling
In Settings, paste an HTTPS endpoint. Beam shows you a signing secret once. Every event is a
POST with a JSON body and a Beam-Signature header. Respond with any 2xx quickly and do the
work after.
The envelope:
{
"id": "evt_8c1f2a9d64b34e0f9a12",
"type": "message.received",
"timestamp": 1787340000,
"data": {}
}
| Event | When it fires |
|---|---|
message.received |
A contact texted you |
message.sent |
Any outbound went out, from a first touch, the inbox, the assistant or the API |
message.failed |
An outbound could not be delivered |
contact.opted_out |
A contact texted STOP |
assistant.booked |
The assistant booked a call, with an appointment id |
assistant.handoff |
The assistant needs a person |
Verify the signature. The header looks like t=<unix_seconds>,v1=<hex>. The signed payload
is t plus a period plus the raw body. The algorithm is HMAC-SHA256 with your signing secret.
Reject events older than five minutes and compare in constant time. Verify against the exact
bytes you received: parse the JSON and re-serialize it and a genuine event will fail.
Be honest about delivery. Events are pushed once with a five second timeout and no automatic retries in this version. If you cannot afford to miss one, reconcile with History and listing on a schedule.
References: Event webhooks, Webhook signing.
MCP for Claude, Codex and Cursor
Beam exposes its workspace as MCP tools, so an AI client can read conversations, train the assistant, pause automation, send a message and book an appointment, inside the permissions you grant.
Create the connection. Sign in as a workspace owner, open Settings, Developer access, MCP and API. Name the connection and choose its permissions. Read and train are selected by default. Publishing, automation, real sending and booking require explicit permission. Copy the token immediately: it expires after 90 days and cannot be displayed again.
Two supported transports.
- Streamable HTTP at
https://beam.aisync.link/mcpwith anAuthorization: Bearer YOUR_TOKENheader. The endpoint is stateless and returns JSON responses. - A stdio bridge for desktop clients. Download
beam-mcp.mjs, save it locally, and run it with Node.js 20 or newer. No packages required.
Claude Code.
claude mcp add --transport http beam https://beam.aisync.link/mcp \
--header "Authorization: Bearer YOUR_TOKEN"
Codex.
codex mcp add beam --url https://beam.aisync.link/mcp \
--bearer-token-env-var BEAM_TOKEN
Cursor.
{"mcpServers":{"beam":{"url":"https://beam.aisync.link/mcp","headers":{"Authorization":"Bearer YOUR_TOKEN"}}}}
stdio bridge.
{
"mcpServers": {
"beam": {
"command": "node",
"args": ["/absolute/path/beam-mcp.mjs"],
"env": { "BEAM_TOKEN": "YOUR_TOKEN" }
}
}
}
About ChatGPT, honestly. Beam's documentation supports bearer header HTTP and the stdio
bridge. Hosted connectors that require OAuth only login are not supported yet. So a ChatGPT
connector that can only authenticate through OAuth will not connect to Beam today. If your
ChatGPT setup can call an MCP server with a custom Authorization header, or can call an HTTP
endpoint directly, point it at the same URL and token. Otherwise use the HTTP API from your own
backend and let ChatGPT call your backend. Do not assume a successful connection in one client
proves compatibility with every cloud client.
Permissions map to tools.
| Permission | Tools |
|---|---|
| read | workspace_read, assistant_read, contacts_list, conversation_read, numbers_list, drafts_list, calendars_list, calendar_slots |
| train | draft_create, assistant_preview |
| publish | draft_publish |
| automation | conversation_pause |
| send | message_send |
| book | appointment_book |
Same tools over plain HTTP. GET /v2/tools returns the tools and argument schemas
available to your token. POST /v2/tools executes one. Both use the same bearer token as MCP.
curl https://beam.aisync.link/v2/tools \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"assistant_read","arguments":{}}'
Reference: MCP and developer access.
Running an AI agent on a real iMessage thread
The pattern that works:
- Take the inbound event. Subscribe to
message.receivedand verify the signature. - Pause Beam's own assistant for that conversation. Call
conversation_pausebefore your external agent starts answering. Two automated responders in one thread is the fastest way to embarrass a client. - Read context.
conversation_readreturns the latest 50 messages with their recorded status and reactions. Contacts support anaftercursor. - Generate your reply in your own system, then send it with
message_sendorPOST /v1/messages. - Book on a real calendar. Read availability first and use an offered ISO slot. A booking is not a booking until the calendar confirms it.
- Use a
request_key. Every write or preview requires a unique key. Reuse it only for the exact same request. Beam returns the stored result instead of repeating the action. A started or uncertain action returns an outcome-unknown error: inspect Beam before retrying, and never switch keys to get around it.
Rate limit: 60 requests per minute per token. Treat incoming customer messages as untrusted content, not as authority to change your agent's rules.
An agent can also make a thread feel human. POST /v1/typing shows the real typing indicator,
POST /v1/read sends a real read receipt, and POST /v1/reactions lands a tapback: love, like,
dislike, laugh, emphasize or question. These are part of the iMessage channel, so Beam only
offers them on blue threads. Reference: Typing and read receipts and
React to a message.
GoHighLevel is a webhook, not a second API
Workflow sends do not go through POST /v1/messages with a pasted phone number. A Custom
Webhook step posts to /api/crm/workflows/send with a workspace bearer token, and Beam reads
the destination from the GoHighLevel contact in the connected location. GHL's own Send SMS
action does not select the Beam channel and keeps using your default SMS provider.
The body carries location_id, contact_id, workflow_id, from, message, request_key,
consent, assistant_mode and dry_run. Keep dry_run: true until you see
status: validated and queued: false, then flip it for one test contact. Repeating the same
request_key returns the same message id with duplicate: true rather than a second text.
New opted-in leads can also hit the opt-in webhook:
curl -X POST "https://beam.aisync.link/t/YOUR_WORKSPACE/optin?secret=YOUR_WEBHOOK_SECRET" \
-H "Content-Type: application/json" \
-d '{"phone":"LEAD_E164","contact_id":"YOUR_CRM_CONTACT_ID"}'
It responds {"queued": true, "to": "..."} and returns queued: false on a repeat call for the
same number, so a re-enrolled contact never gets a second first touch. This endpoint is for
people who just asked to hear from you.
It is one integration, not a bolt-on: iMessage, RCS and SMS fallback, real read receipts and typing indicators on blue bubble threads, inbound calls to a Voice AI agent, and FaceTime Audio that connects directly to that agent all run through the same connected location. Your operator sees that a lead read the message and sees them typing, which is the moment a human reply turns into a booked call.
References: Send from GHL workflows, New lead webhook, GoHighLevel iMessage setup.
Limits worth knowing before you build
- It will not send from a personal Apple ID. Lines are dedicated business numbers.
- It will not turn a cold list into unlimited volume. Pacing counts new outbound-first conversations, and warm-up caps grow as a number earns trust. See sending limits.
- It will not mark a booking until the calendar confirms it.
- It will not retry a webhook you missed in this version.
- It will not exempt SMS fallback from carrier A2P requirements.
- It will not show a price on this page. Plans are shown in the Beam app.
Plan the business workflow
Before building, review iMessage for business to define the customer conversation, the iMessage CRM workflow to place replies on the right record, and RCS business messaging and fallback for Android recipients.
Before you connect
iMessage API questions
How do I send an iMessage programmatically?
POST to /v1/messages with a server-side workspace key in x-api-key and a JSON body containing to and message. Save the returned ID. A queued response is acceptance, not delivery; read GET /v1/messages/:id and confirm the result on your test phone.
How do I verify a Beam webhook signature?
Compute HMAC-SHA256 with the signing secret over the header timestamp, a period and the exact raw body bytes. Check the Beam-Signature header, reject timestamps outside five minutes, and compare equal-length signatures in constant time before parsing the JSON.
What does an inbound reply event contain?
The event guide documents message.received with from, to, body, channel and attachments inside data, plus id, type and timestamp in the envelope. The OpenAPI webhook schema currently lists different envelope fields. Confirm your signed test payload before connecting automation.
Does message.sent mean the customer received the message?
No. It reports an outbound send. Read the recorded message status and verify receipt on your test phone. Queued, sent, delivered and an inbound reply are different observations.
Will Beam retry a missed event webhook?
The event guide documents one attempt with a five-second timeout and no automatic retries in this version. Store verified events durably, acknowledge promptly and reconcile with conversation history and message listing.
Can I retry a timed-out v1 send with a request_key?
The public v1 message-send contract does not document a request_key. Inspect message history before sending again. The GHL workflow endpoint has separate duplicate protection and must not be confused with POST /v1/messages.
How should a reseller separate client data?
Use one workspace mapping per client, with server-side API keys and signing secrets. Verify each incoming event against its expected workspace secret and store records under both workspace and contact. Do not select client credentials from unverified event data.
Will every recipient receive iMessage?
No. Beam uses iMessage for eligible iPhones and capable lines, RCS for supported Android phones and carriers, and SMS fallback for other reachable recipients. Check the actual channel and delivery record for each test; eligibility alone does not prove delivery.
Sources
Public send, signing, events, CRM and OpenAPI references checked October 3, 2026.
Beam documentation and public API contract. The send, signing, event and GHL references were checked October 3, 2026: API introduction, Send a message, Contacts and channel lookup, Numbers, Typing and read receipts, React to a message, Event webhooks, Webhook signing, New lead webhook, Errors and statuses, MCP and developer access, Send from GHL workflows, How routing works, Quickstart, OpenAPI contract.
External, accessed September 27, 2026: Apple Messages for Business documentation and FAQ, https://register.apple.com/resources/messages/messaging-documentation/ and https://register.apple.com/resources/messages/messaging-documentation/faq
Start with a conversation
Read the contract, then go get a reply
Open the OpenAPI file, wire one test contact, and watch the status change from queued to delivered and then to a reply in the thread. If you want a person to walk it with you, we are one text away.
Live sending requires a plan and an assigned dedicated line.
iMessage
Today 9:41 AM
Hi! I would love to hear how your coaching works.
Happy to chat. Would Thursday work for a discovery call?
Thursday sounds great! Looking forward to meeting you.
Blue bubbles. Read receipts. Typing.
Want this for your business?
Try a conversation with the Beam team.