Project Blue API
Send iMessage and SMS from your own application. Integrate messaging into your CRM, workflows, or custom tools with a single API call.
The API is organised around REST. It accepts JSON request bodies, returns JSON responses, and uses standard HTTP verbs and status codes. Every request is authenticated with a bearer token.
/llms/<section>.md — the View as Markdown link at the top of each section points at it. Point your agent at those rather than scraping the page.MCP server & skill
Working with an AI assistant? Connect it here first — this is the fastest way to send a message, and there is no API key to paste.
The Project Blue MCP server gives Claude Desktop, Claude Code, Cursor, and any other MCP-compatible client the ability to send iMessage and SMS, place FaceTime Audio calls, manage contacts, run Workflows, and read message and call history — directly from the editor or chat surface. Authentication is OAuth 2.1 in the browser.
Writing code against the REST API instead? Start with the Quickstart.
Two ways to connect
Add the server directly. This is the standard path and works in any MCP client — paste the URL, or run the one-liner in the rail. Nothing else is installed.
Or install the plugin (Claude Code only). It registers the same server and adds a Project Blue skill carrying the workflow rules the tool descriptions cannot: the trial verification sequence, when a retry is being deduplicated rather than failing, the FaceTime call lifecycle, and which contact endpoint merges versus replaces.
claude plugin install is a convenience, not a requirement — if you already added the server with claude mcp add, it keeps working and there is nothing to migrate. Installing both would simply register the server twice, so pick one.Set up your client
Pick your client in the rail and run the snippet. Every one of them ends the same way: your client opens a browser to app.tryprojectblue.com, you approve the connection, and it is done. OAuth 2.1 with dynamic client registration — no key to paste, no tokens to rotate.
claude mcp add, or install the plugin belowcodex mcp add then codex mcp login, or ~/.codex/config.toml~/.cursor/mcp.json or .cursor/mcp.json.vscode/mcp.jsonAny other client that speaks remote MCP over streamable HTTP works with the server URL alone. For one that only accepts stdio servers, use mcp-remote as an adapter — both are in the rail.
Available tools
send_messagesend_group_messagelookup_imessage_availabilityget_lineslist_messagesget_messageget_call_logsstart_facetime_callget_facetime_call_statusend_facetime_callcreate_external_contactget_external_contactsupdate_external_contactlist_flowsenroll_contact_in_flowcancel_flow_runsQuickstart
Four steps from an API key to a delivered message. Every call here is copy-paste ready.
1. Get a key, then check it
Create a key in the Project Blue dashboard under Settings → API Keys. Keys start with proj_ followed by 64 hex characters, and you may hold five active keys at a time.
Start with /get-lines. It is the right first call because it reads rather than sends, and because its answer tells you which of the two setups you are in.
trial: true401The two 401 bodies mean different things. Missing or invalid Authorization header means the header was absent or lacked the Bearer prefix — the request never carried a key. Invalid API key means the header was well-formed but the key is wrong, revoked, or from another account.
2. Get a number you can text
On a paid account, skip this — any valid number works.
On a trial, sends route through a shared Project Blue line and can only reach verified destinations. Verification is confirmed by an inbound text: the owner of that number must text the shared line from their own phone. Nothing in the API or the dashboard can confirm it on their behalf.
Sending to an unverified destination returns 403 with Trial accounts can only message numbers verified on the shared line. See Trial accounts for what else is restricted.
3. Send the message
Two fields is the whole request. If the recipient has iMessage it arrives as one; otherwise it falls back to SMS automatically.
A success returns status: "done" — but no message id, which is why there is a step 4.
4. Confirm it landed
The send response tells you the message was accepted, not that it was delivered. Read it back from list messages filtered to the number you texted, and check data[0].status. The same row carries the message_handle you need for get a message.
In production, register a webhook instead of polling.
- Re-running the same send is collapsed, not repeated. Identical text to the same number within the hour returns
200withdeduped: trueand sends nothing. Change the text or pass a distinctidempotencyKey. This is the most common reason a first integration looks like it worked but nothing arrived. - 60 requests per minute per key, then
429withretryAfterSeconds. - Numbers are normalized to E.164. Most formats are accepted on the way in; everything comes back as
+15551234567. - Trial sends are real messages on a line shared with other accounts. Do not load-test them — unverified probing burns iMessage reputation for everyone on that line.
Authentication
All API requests require a bearer token in the Authorization header. You can generate API keys from within your Project Blue dashboard under Settings → API Keys.
Trial accounts
Trial accounts (accounts still on a trial, before conversion to a paid line) get real API access — not a simulator. Sends are real messages. The difference is that a trial account sends through a shared Project Blue line, and it can only reach destinations that have been verified.
Verifying a destination
- Register the destination in the Project Blue webapp under Settings. This creates a pending verification pinned to your shared line.
- The owner of that destination texts the shared line from their own phone. That inbound text is what confirms the verification. There is no way to confirm it from the API or the dashboard.
- The destination shows as verified in the webapp, and API sends to it start succeeding.
Destination registration is webapp-only today — there is no API-key equivalent for registering a number or polling verification status yet.
Sending or probing an unverified destination returns 403 with Trial accounts can only message numbers verified on the shared line.
Endpoint availability
/send-api-message/api-check-imessage-availability/get-lines/get-messages-api/get-message-api/:message_handle/create-external-contact/get-external-contacts/update-external-contact/get-call-logs-api/get-flows/cancel-flow-runsWebhooks/send-group-message/enroll-flow/start-facetime-call-api/get-facetime-call-status-api/end-facetime-call-apilineId on /send-api-message returns 400 — see the rail.GET /get-lines on a trial account
Trial accounts do not own a device line. /get-lines returns a trial envelope instead of an array of lines.
Send a message
Send a message to a phone number via iMessage or SMS. If the recipient has iMessage, the message is delivered as an iMessage (blue bubble). Otherwise, it falls back to SMS automatically.
messagestringphonestringRequiredlineIdstring (UUID v4)mediaAttachmentUrlstringaudioAttachmentUrlstringenableAiVoiceMemobooleanshouldAutoCreateContactbooleanfirstNamestringlastNamestringemailstringcustomFieldsobjectidempotencyKeystringThe phone parameter is flexible — we accept formats like (555) 123-4567, 555.123.4567, +15551234567, and more. All numbers are normalized to E.164 format before sending.
When lineId is provided, we route through that line. If it is omitted, message sends continue to use default load balancing across your available lines.
idempotencyKey, the key defaults to your user id plus the destination plus the message text. Sending the same text to the same number twice within the hour returns 200 with deduped: true and does not send a second message. This is the usual reason a first integration looks like it succeeded but nothing arrived on the retry — change the text, or pass a distinct idempotencyKey, when a repeat is intentional.A replay returns status: "done" when the original send finished, or status: "processing" with messageType and devicePhoneNumber still null while it is in flight. Both carry deduped: true; a first-time send never does.
Send a group message
Send an iMessage or SMS to a group of 2 to 32 recipients. There is no create step — pass numbers every time. The same participant set resolves to the same thread.
status is always "queued" and service is always null on this endpoint. The send is never synchronous — whether the thread lands as iMessage or SMS is not known until the cron runs the all-participants availability probe. Read the resolved service from /get-messages-api./send-api-message.numbersstring[]RequiredmessagestringmediaAttachmentUrlstringaudioAttachmentUrlstringenableAiVoiceMemobooleangroupNamestringlineIdstring (UUID v4)idempotencyKeystringgroupStatus is "creating" until the chat exists on the sending line, then "active". created is true only when this call created the group. groupId is opaque and stable — do not parse it.
On a replayed or deduplicated request, groupId and devicePhoneNumber can be null, because the group did not exist yet when the claim was taken.
Group send is unavailable on trial accounts — see Trial accounts.
- 200
- Message queued successfully
- 400
- Invalid request — missing message/attachment, both attachments, recipient count, email/chat handles, phone format, or lineId
- 401
- Missing or invalid Authorization header / Invalid API key
- 403
- Unavailable on trial (see Trial accounts) — e.g. Trial accounts cannot create group chats. Also: One or more recipients are blocked.
- 409
- This group already exists on a different line.
- 429
- Rate limit exceeded
- 500
- Internal server error
CRM integration
The send endpoint works hand-in-hand with your CRM. If you have HighLevel or HubSpot connected, outbound messages sent through the API appear inside your CRM — just like messages sent from the Project Blue app.
Auto-create contacts
By default, shouldAutoCreateContact is true. This means if you send an outbound message and the recipient does not already exist as a contact in your CRM, we will automatically create the contact for you along with the message.
Set shouldAutoCreateContact to false if you only want messages logged for contacts that already exist in your CRM.
shouldAutoCreateContact has no effect. Messages are still sent normally regardless of this setting.HighLevel
Outbound messages are synced directly into Conversations. If the contact doesn't exist and auto-create is enabled, we create the contact and the message appears in their conversation thread.
HubSpot
Outbound messages are logged as an activity on the contact record. If you have a HubSpot Inbox enabled, the message is also delivered there for your team to see and reply from.
If the contact doesn't exist and auto-create is enabled, we create the contact in HubSpot first, then log the activity.
List messages
Returns a paginated list of the authenticated user's messages — outbound and inbound merged into a single feed. Each message includes a durable message_handle that can be passed to /get-message-api/:message_handle for the full record.
limitinteger (1–100)offsetinteger (≥ 0)order_by"createdAt" | "sentAt"order_direction"asc" | "desc"service"iMessage" | "SMS" | "RCS"direction"inbound" | "outbound"pb_line_idstringfrom_numberstring (E.164)to_numberstring (E.164)created_at_gtestring (ISO-8601)created_at_ltestring (ISO-8601)sent_at_gtestring (ISO-8601)sent_at_ltestring (ISO-8601)On outbound group messages, to_number is the group's chat identifier (chat…), not an E.164 number. On inbound group messages, from_number is the individual participant who replied. The to_number and from_number filters are therefore not a way to fetch a group thread.
There is currently no supported filter for reading one group thread from /get-messages-api. The pbg_ groupId is not accepted as a filter, and the chat identifier is not exposed as a queryable field on this API.
message_handle is an opaque, user-scoped identifier. Don't try to parse or decode it — just hand it back to /get-message-api to look up that specific message.Get a message
Fetch a single message by its opaque message_handle (as returned from /get-messages-api). Handles are scoped to the authenticated user — a handle from another user's account returns 404.
message_handlestringRequiredCheck iMessage availability
Check whether a phone number is reachable via iMessage before sending. Useful for routing logic or pre-qualifying contacts.
phonestringRequired- 200
- Phone checked successfully
- 400
- Invalid phone number format
- 401
- Missing or invalid API key
- 403
- Trial account probing a destination that is not verified on the shared line
- 409
- Trial account has no shared line provisioned
- 429
- Rate limit exceeded
- 500
- Internal server error
Get lines
Fetch all sending lines available to your account. Pass the returned lineId value to /send-api-message when you want to force a specific line.
Create a contact
External contacts are Project Blue's native contact store for accounts without a connected CRM. They render in the Project Blue app's contact list and details panel.
You can create them explicitly with the endpoints below, or implicitly by passing firstName, lastName, email, and customFields on /send-api-message — those fields persist on the auto-created contact for the recipient.
Creates a contact, or idempotently upserts by phone number if one already exists. Use this when you want the contact to exist before any message is sent.
firstNameandlastNameare only filled in when the stored value is empty. Sending a new name for a contact that already has one is silently discarded.emailoverwrites whenever you send a non-empty value.customFieldsis shallow-merged here — the opposite of update, which replaces the whole object. Sending{}changes nothing.
The response is the contact as it was before the merge, so a discarded name is not visible in it. Read the contact back if you need to confirm. The same merge runs when /send-api-message auto-creates a contact.
phonestringRequiredfirstNamestringlastNamestringemailstringcustomFieldsobjectAll three endpoints return the same object. Create and update return it as { contact }; list returns { contacts, pagination }. There is no status envelope.
idstringRequiredfirstNamestring | nullRequiredlastNamestring | nullRequiredphoneNumberstringRequiredemailstring | nullRequiredcustomFieldsobject | nullRequirednotestring | nullRequiredcreatedAtstring (ISO-8601)RequiredupdatedAtstring (ISO-8601)RequiredList contacts
Returns the authenticated user's external contacts, newest first, with pagination. Pass phone to look up a single contact by number.
limitinteger (1–100)offsetinteger (≥ 0)phonestringUpdate a contact
Updates an existing contact's name, email, note, or custom metadata. The contact's phone number cannot be changed. Returns 404 if the contact does not belong to your account.
At least one of firstName, lastName, email, customFields, or note must be present. A request carrying only contactId returns 400. The phone number cannot be changed.
contactIdstringRequiredfirstNamestringlastNamestringemailstringnotestringcustomFieldsobject400 bodies come out of the same check, on every endpoint that accepts customFields: customFields must be a JSON object of key/value pairs when the value is not a plain object, customFields must be a JSON-serializable object when it contains something that cannot be stringified, and customFields JSON exceeds maximum length of 10000 characters past the size cap. That cap counts characters of serialized JSON, not bytes — non-ASCII values reach it later than their byte size suggests.customFields on an update overwrites the entire stored object. To add or change one key, fetch the contact first, merge on your side, and send the full object back.List flows
Workflows are automation flows built in the Project Blue app — send a message, wait, branch on a reply, and so on. These endpoints list your published flows, enroll a contact (which starts the run immediately), and cancel active runs for a contact.
pb_line_id is omitted, a line is selected automatically at enrollment and pinned for the entire run; the chosen line is returned in the response.Returns the authenticated user's published Workflows. Use each flow's id as flowId when enrolling a contact. Ids are opaque UUID strings, not sequential numbers — pass them back verbatim.
Enroll a contact
Starts a Workflow run for the given contact. The first step executes immediately. The response includes runId and the pb_line_id pinned to the run.
Because the first step runs before the response is written, the returned status is the state after that step. A run that begins with a wait comes back SLEEPING, and one that begins by asking a question comes back WAITING_REPLY — ACTIVE is the exception, not the rule. Statuses are uppercase: ACTIVE, SLEEPING, WAITING_REPLY, COMPLETED, CANCELLED, FAILED.
flowIdstring (UUID)RequiredphoneNumberstringRequiredpb_line_idstring (UUID)- 400
- Invalid phone number, invalid pb_line_id, or no available lines
- 403
- Contact has opted out
- 404
- Flow not found or not published, or line not found
- 409
- An active run already exists for this contact in this flow
Cancel flow runs
Cancels all active Workflow runs for the given contact. Safe to call when the contact has no active runs. POST is accepted as an alias for clients that cannot send DELETE with a body.
phoneNumberstringRequiredCall logs
Returns the authenticated user's outbound call logs from the Project Blue dialer, including a recording_url when a recording is available. This is the call-log analog of /get-messages-api.
This endpoint reports call attempts, not just recorded calls — so unanswered, busy, failed, and very short calls all show up with recording_url: null. Answered calls typically populate recording_url within seconds of ended_at.
If a long-completed call still has recording_url: null, it usually means no audio was captured for that call (e.g. recording disabled on the line).
limitinteger (1–100)offsetinteger (≥ 0)call_log_timestamp"asc" | "desc"pb_line_idstring (UUID)answered_at_gtestring (ISO-8601)answered_at_ltestring (ISO-8601)idstringRequiredline_idstring | nullRequiredfrom_numberstringRequiredto_numberstringRequiredstatusstringRequireddispositionstring | nullRequiredtranscriptstring | nullRequiredduration_secondsnumber | nullRequiredanswered_atstring (ISO-8601) | nullRequiredended_atstring (ISO-8601) | nullRequiredrecording_urlstring | nullRequiredDispositions
After a recording is transcribed, the transcript is run through an AI classifier that assigns one of the following dispositions. Use this for routing, follow-up automation, or analytics. The disposition can be null on calls that haven't been classified yet.
answeredvoicemailbusyno_answerwrong_numbernot_interestedcallback_requestedmeeting_scheduledinformation_providedno_speechunknown- 200
- Call logs returned
- 400
- Invalid query parameter (limit/offset/call_log_timestamp/dates/pb_line_id)
- 401
- Missing or invalid API key
- 429
- Rate limit exceeded
- 500
- Internal server error
FaceTime Audio
FACETIME_NOT_ENABLED — that means the feature isn't active on your account, not that the API is down. Interested in FaceTime Audio? Contact sales@tryprojectblue.com or reach out to support to upgrade.Initiate real FaceTime Audio calls programmatically and connect to the live call audio via WebRTC using Agora's SDK. The pb_line_id must be a FaceTime-enabled line on your account.
pb_line_idstring (UUID)Requiredphone_numberstring (E.164)Requiredstatus"OK"Requiredcall_uuidstringRequiredagoraobject | nullRequiredappIdstringRequiredchannelNamestringRequiredtokenstringRequireduidnumberRequiredJoining the call
The returned agora credentials are used with the Agora Voice SDK to stream audio to and from the FaceTime call. Tokens are time-limited, so join the channel promptly after starting the call.
200 means the call was placed, not that you can hear it. When the device returns no credentials, agora is null and the call still rings — check for it before touching the SDK, and fall back to polling /get-facetime-call-status-api for the outcome.FaceTime call status
Status is driven by call lifecycle events. Poll this endpoint to track ringing → answered → ended.
call_uuidstringRequiredEnd a FaceTime call
call_uuidstringRequirederror_code: FACETIME_DAILY_LIMIT_REACHED plus uniqueDestinationsToday and limit fields.- 200
- Call started / status returned
- 400
- Invalid pb_line_id or phone_number
- 401
- Missing or invalid API key
- 403
- FACETIME_NOT_ENABLED — FaceTime Audio not enabled on this account
- 404
- Call not found (status/end endpoints)
- 429
- Rate limit exceeded or daily FaceTime dial limit reached
- 502
- Device error placing the call
- 500
- Internal server error
Calling
Voice calling via the dialer is not provided by the Project Blue API. Outbound and inbound calls are handled by Twilio. We give you a Twilio API key in the webapp so you can build calling yourself.
FaceTime Audio calls are provided natively by the Project Blue API — see FaceTime Audio above.
Your Twilio API key is already created for you. You don't add phone numbers or caller ID — our team provides those.
Getting your Twilio API key
You must be on an API or Zapier based account.
In the Project Blue portal, go to Settings → API Keys. Your Twilio API key is exposed there.
Use Twilio's Voice API documentation to implement outbound and inbound calls.
Webhooks
Webhooks let you receive real-time notifications when messages are sent or received. Configure webhooks from within the Project Blue dashboard alongside your API keys.
Configuration
In the Project Blue app, you can:
- Paste the webhook URL you want to receive events at
- Toggle whether the webhook fires for outbound messages, inbound messages, or both
- Send test payloads to verify your endpoint is working
Delivery
Your endpoint should answer 2xx. Anything else — including a network error or a timeout — counts as a failure.
There are no retries. Each event is delivered exactly once; a failed delivery is not queued or replayed, so a webhook is not a durable log. Reconcile with /get-messages-api if you need guaranteed coverage.
After 20 consecutive failures the webhook is automatically disabled. The counter resets on the first success. Today that happens silently — there is no email and no dashboard banner, so check the Webhooks tab if events stop arriving.
Webhook payload
The direction field indicates whether the message was inbound or outbound.
messagestringRequireddestinationstringRequiredreceivedAtstringRequireddirectionstringRequiredmessageIdnumberRequiredguidstringRequiredlinePhoneNumberstringRequiredcontactPhoneNumber, dateReceived (a legacy alias for receivedAt), hubspotContactId (number), and hubspotContactIdText (the same id as a string). Those webhooks have no inbound/outbound toggles and are not covered by the auto-disable behaviour described above.Supported media
Use the mediaAttachmentUrl field to send rich media with your messages. The following formats are supported.
Images
Videos
Contact cards
Voice memos & audio
Use audioAttachmentUrl to send audio files as voice memos. The following formats are supported.
enableAiVoiceMemo to true and include a message to generate a natural-sounding voice memo via text-to-speech. No audio file needed — we generate it for you.Rate limits
Every API-key endpoint allows 60 requests per minute, counted per API key in a rolling 60-second window.
Each response carries RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset. A rejected request also carries Retry-After, and the body repeats the wait as retryAfterSeconds.
Authorization header, and that happens before the key is validated. Requests made with a revoked or mistyped key still consume that token's budget, and requests with no Authorization header at all share a single per-IP bucket.error_code: FACETIME_DAILY_LIMIT_REACHED. Branch on error_code rather than on the status alone: waiting retryAfterSeconds will never clear the daily cap.Error handling
The API uses standard HTTP status codes. All error responses include a JSON body with an error field describing what went wrong.
- 200
- Message sent successfully
- 400
- Invalid request body or missing required fields
- 401
- Missing or invalid API key
- 403
- Forbidden — e.g. endpoint unavailable on trial accounts (see Trial accounts), unverified trial destination, or opted-out contact
- 409
- Conflict — a group already exists on another line, a Workflow run is already active for this contact, or a trial account has no shared line provisioned
- 429
- Rate limit exceeded (60 requests/minute per key), or the FaceTime daily dial cap
- 500
- Internal server error
Two error bodies are worth special-casing. Pagination depth too deep means offset + limit exceeded 2000 on list messages or call logs — narrow the query by date or line rather than paging deeper. This trial account is not provisioned on a shared line is a 409 that only trial accounts see; it means the destination is verified but the account has no shared line yet.