Beam

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:

  1. Queued is not delivered. A 200 means the send was accepted into a paced queue. Poll GET /v1/messages/:id or subscribe to webhooks for the real status. sent is not delivered.
  2. 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.
  3. 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 lineRoute to evaluateEvidence to retain
Eligible iPhone and capable lineiMessageRecorded channel, delivery state and a phone reply
Android phone and carrier support RCSRCSActual send channel and test-phone receipt
Other reachable recipientSMS fallbackLine eligibility and recorded SMS result
Ambiguous provider updateNo automatic resend or downgradeReconcile 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.

  1. Configure an HTTPS receiver in Settings, Event webhooks. Store the signing secret on your server. It is separate from the API key.
  2. Run the send request below after replacing both placeholders. Save the returned message ID.
  3. Read that ID with the status request. Observe the receiving phone; an accepted request or message.sent event is not a delivery receipt.
  4. 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.
  5. 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

ObservationWhat it provesWhat to do next
HTTP 200 and queuedBeam accepted the send into its queueSave the ID and inspect GET /v1/messages/:id
message.sentAn outbound message went outDo not label it delivered solely from this event
Recorded delivered statusThe message record reports deliveryCompare with the test phone; do not infer a read or reply
Verified message.receivedA reply event passed signature checksMatch the workspace and phone pair before updating the conversation
Failed, no_channel or uncertain resultThe desired outcome is not establishedInspect 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

  1. 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.
  2. Check the number. GET /v1/availability/:phone tells you whether a phone supports blue bubbles before you spend a send.
  3. Send. POST /v1/messages with to and message.
  4. Read the status. GET /v1/messages/:id, or better, take the signed webhook.
  5. 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/mcp with an Authorization: Bearer YOUR_TOKEN header. 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:

  1. Take the inbound event. Subscribe to message.received and verify the signature.
  2. Pause Beam's own assistant for that conversation. Call conversation_pause before your external agent starts answering. Two automated responders in one thread is the fastest way to embarrass a client.
  3. Read context. conversation_read returns the latest 50 messages with their recorded status and reactions. Contacts support an after cursor.
  4. Generate your reply in your own system, then send it with message_send or POST /v1/messages.
  5. 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.
  6. 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.

    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.

    Interactive demo · Simulation
    CCClearpath Coaching

    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.

    Type your own, or send a suggestion.

    LET’S TALK

    Want this for your business?

    Try a conversation with the Beam team.

    We use your number to prepare your text link and follow up. No spam. Privacy.