Send iMessages and SMS directly from your application. Integrate messaging into your CRM, workflows, or custom tools with a single API call.
https://api.tryprojectblue.com
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.

Authorization: Bearer YOUR_API_KEYKeep your API keys secure
Never expose your API key in client-side code or public repositories. Always make API calls from your server.
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.
| Parameter | Type | Required | Description |
|---|---|---|---|
message | string | Required | The text content of the message to send. |
phone | string | Required | Recipient phone number. Accepts many formats — we normalize to E.164 (e.g. +15551234567). |
lineId | string (UUID v4) | Optional | Optional sender line override. Use the lineId value returned from GET /get-lines. When omitted, messages are load balanced across available lines. |
mediaAttachmentUrl | string | Optional | URL to an image, video, or contact card attachment. |
audioAttachmentUrl | string | Optional | URL to an audio file sent as a voice memo. |
enableAiVoiceMemo | boolean | Optional | When true, generates an AI voice memo from the message text using text-to-speech. |
shouldAutoCreateContact | boolean | Optional | Defaults to true. When enabled and a supported CRM is connected (HighLevel or HubSpot), automatically creates the contact in your CRM if they don't already exist. |
The 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.
curl -X POST https://api.tryprojectblue.com/send-api-message \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Hey! Just following up on our conversation.",
"phone": "+15551234567"
}'curl -X POST https://api.tryprojectblue.com/send-api-message \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Check out this property!",
"phone": "+15551234567",
"mediaAttachmentUrl": "https://example.com/property-photo.jpg"
}'curl -X POST https://api.tryprojectblue.com/send-api-message \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Hey, wanted to quickly touch base about your appointment tomorrow.",
"phone": "+15551234567",
"enableAiVoiceMemo": true
}'curl -X POST https://api.tryprojectblue.com/send-api-message \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Hi from a specific line.",
"phone": "+15551234567",
"lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5"
}'{
"success": true,
"message": "Message added to queue",
"messageType": "iMessage",
"phone": "+15551234567",
"devicePhoneNumber": "+15559876543",
"mediaAttachmentUrl": null,
"audioAttachmentUrl": null
}{
"error": "Invalid lineId. Expected a UUID v4 token returned from GET /get-lines."
}The Send Message endpoint works hand-in-hand with your CRM. If you have HighLevel or HubSpot connected, outbound messages sent through the API will automatically appear inside your CRM — just like messages sent from the Project Blue app.
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.
This field is only relevant if you have a supported CRM connected
If no CRM is connected, shouldAutoCreateContact has no effect. Messages are still sent normally regardless of this setting.
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.
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.
curl -X POST https://api.tryprojectblue.com/send-api-message \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Hey! Following up on our call earlier.",
"phone": "+15551234567",
"shouldAutoCreateContact": false
}'In the example above, the message will only be logged in your CRM if the contact already exists. No new contact will be created.
Check whether a phone number is reachable via iMessage before sending. Useful for routing logic or pre-qualifying contacts.
| Parameter | Type | Required | Description |
|---|---|---|---|
phone | string | Required | The phone number to check. Accepts many formats — we normalize to E.164. |
curl -X POST https://api.tryprojectblue.com/api-check-imessage-availability \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+15551234567"
}'{
"normalizedPhone": "+15551234567",
"isIMessageAvailable": true
}{
"error": "Invalid phone number format (Not a valid phone number). Please use format +12345678901"
}Phone checked successfully
Invalid phone number format
Missing or invalid API key
Rate limit exceeded
Internal server error
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.
curl -X GET https://api.tryprojectblue.com/get-lines \
-H "Authorization: Bearer YOUR_API_KEY"[
{
"lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5",
"devicePhoneNumber": "+15559876543",
"customName": "Main Line"
},
{
"lineId": "9c2e4a1b-d3f8-4e1c-a2b4-c5d6e7f8a9b0",
"devicePhoneNumber": "+15557654321",
"customName": "Secondary Line"
}
]{
"error": "Missing or invalid Authorization header"
}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.
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | integer (1–100) | Optional | Maximum number of messages to return. Defaults to 100. |
offset | integer (≥ 0) | Optional | Pagination offset. Defaults to 0. |
order_by | "createdAt" | "sentAt" | Optional | Sort field. Defaults to createdAt. |
order_direction | "asc" | "desc" | Optional | Sort direction. Defaults to desc (newest first). |
service | "iMessage" | "SMS" | "RCS" | Optional | Filter by delivery service. Note: RCS is inbound-only — combining service=RCS with direction=outbound returns zero results. |
direction | "inbound" | "outbound" | Optional | Filter to inbound or outbound only. Omit for both. |
pb_line_id | string | Optional | Encoded Project Blue line id (from GET /get-lines). The only supported way to filter by one of your own PB lines — do not use from_number/to_number for that. |
from_number | string (E.164) | Optional | External sender. Inbound-only filter. Combining with direction=outbound returns 400. |
to_number | string (E.164) | Optional | External recipient. Outbound-only filter. Combining with direction=inbound returns 400. |
created_at_gte | string (ISO-8601) | Optional | Lower bound on created_at. |
created_at_lte | string (ISO-8601) | Optional | Upper bound on created_at. |
sent_at_gte | string (ISO-8601) | Optional | Lower bound on sent_at. |
sent_at_lte | string (ISO-8601) | Optional | Upper bound on sent_at. |
curl -G https://api.tryprojectblue.com/get-messages-api \
-H "Authorization: Bearer YOUR_API_KEY" \
--data-urlencode "limit=5" \
--data-urlencode "direction=outbound" \
--data-urlencode "service=SMS"{
"status": "OK",
"data": [
{
"message_handle": "pbm_outk6dawyUgbl-EjXCZk-g5mZnmwmSbilbaX-fvceChASMVXv1v26ONS0XENe2ggdJ82j9TMpw",
"content": "Hello this is a message from desktop Claude!",
"from_number": "+14804328406",
"to_number": "+14808405291",
"line_id": "7fd53c9a-5e6f-40e7-48c2-57663bad6c9c",
"service": "SMS",
"direction": "outbound",
"status": "delivered",
"created_at": "2026-04-19T07:30:14.525Z",
"sent_at": "2026-04-19T16:00:31.091Z",
"media_attachment_url": null,
"voice_attachment_url": null
}
],
"pagination": { "limit": 100, "offset": 0, "total": 427 }
}About message_handle
The 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.
{
"error": "direction=inbound cannot be combined with to_number"
}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.
| Parameter | Type | Required | Description |
|---|---|---|---|
message_handle | string | Required | Opaque handle starting with pbm_, returned by /get-messages-api. |
curl https://api.tryprojectblue.com/get-message-api/pbm_outk6dawyUgbl-EjXCZk-g5mZnmwmSbilbaX \
-H "Authorization: Bearer YOUR_API_KEY"{
"status": "OK",
"data": {
"message_handle": "pbm_outk6dawyUgbl-EjXCZk-g5mZnmwmSbilbaX",
"content": "Hello this is a message from desktop Claude!",
"from_number": "+14804328406",
"to_number": "+14808405291",
"line_id": "7fd53c9a-5e6f-40e7-48c2-57663bad6c9c",
"service": "SMS",
"direction": "outbound",
"status": "delivered",
"created_at": "2026-04-19T07:30:14.525Z",
"sent_at": "2026-04-19T16:00:31.091Z",
"media_attachment_url": null,
"voice_attachment_url": null
}
}{
"error": "Message not found"
}{
"error": "Invalid or missing message_handle"
}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.
Outbound dialer calls only
This endpoint returns outbound calls placed from the Project Blue dialer.
recording_url can be null — that's expected
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).
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | integer (1–100) | Optional | Maximum number of call logs to return. Defaults to 100. |
offset | integer (≥ 0) | Optional | Pagination offset. Defaults to 0. |
call_log_timestamp | "asc" | "desc" | Optional | Sort direction by the call log's created_at, so unanswered attempts interleave with answered calls. Defaults to desc (newest first). |
pb_line_id | string (UUID) | Optional | Encoded Project Blue line id (from GET /get-lines). Returns 400 if the line does not belong to the API key's user. |
answered_at_gte | string (ISO-8601) | Optional | Inclusive lower bound on answered_at. Note: only matches calls that were actually answered. |
answered_at_lte | string (ISO-8601) | Optional | Inclusive upper bound on answered_at. Note: only matches calls that were actually answered. |
curl -G https://api.tryprojectblue.com/get-call-logs-api \
-H "Authorization: Bearer YOUR_API_KEY" \
--data-urlencode "pb_line_id=6121307b-c29e-41d5-426b-46b679ab8648" \
--data-urlencode "answered_at_gte=2026-04-01T00:00:00Z" \
--data-urlencode "answered_at_lte=2026-05-01T00:00:00Z" \
--data-urlencode "call_log_timestamp=asc" \
--data-urlencode "limit=50"{
"status": "OK",
"data": [
{
"id": "cmorpmiw5fyb513ynkuz8jr39",
"line_id": "6121307b-c29e-41d5-426b-46b679ab8648",
"from_number": "+16027184932",
"to_number": "+18016966474",
"status": "completed",
"disposition": "answered",
"transcript": "Speaker A: Hey Colton, how are you?\nSpeaker B: This is Camila from Project Blue…",
"duration_seconds": 168,
"answered_at": "2026-05-04T21:24:06.882Z",
"ended_at": "2026-05-04T21:26:53.882Z",
"recording_url": "https://<storage-host>/call-recordings/<...>.mp3"
},
{
"id": "cmorbzz12abcd13ynxxxxxxxx",
"line_id": "6121307b-c29e-41d5-426b-46b679ab8648",
"from_number": "+16027184932",
"to_number": "+18015550199",
"status": "no-answer",
"disposition": "no-answer",
"transcript": null,
"duration_seconds": null,
"answered_at": null,
"ended_at": "2026-05-04T20:11:08.000Z",
"recording_url": null
}
],
"pagination": { "limit": 100, "offset": 0, "total": 910 }
}| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Required | Internal call log id (stable, useful for dedupe). |
line_id | string | null | Required | UUID-encoded PB line id. Round-trips with the pb_line_id filter. |
from_number | string | Required | E.164 sender (your line). |
to_number | string | Required | E.164 recipient. |
status | string | Required | Call status (completed, no-answer, busy, failed, canceled, …). |
disposition | string | null | Required | AI-assigned disposition derived from the call transcript. See the Dispositions table below for all possible values. |
transcript | string | null | Required | Speaker-labelled transcript when one was generated. |
duration_seconds | number | null | Required | Connected duration. null for calls that never connected. |
answered_at | string (ISO-8601) | null | Required | When the call was answered. |
ended_at | string (ISO-8601) | null | Required | When the call ended. |
recording_url | string | null | Required | URL to the call recording when one is available; null otherwise. |
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.
A human answered and spoke (any clear human speech that isn't a voicemail greeting).
The call went to voicemail (voicemail greeting, beep, or 'leave a message' prompt detected).
The line was busy.
No one answered — just ringing or silence.
The person on the other end indicated this is the wrong number.
The person explicitly declined or showed no interest.
The person asked to be called back at a later time.
A meeting or appointment was scheduled on the call.
Information was exchanged but no clear next step was set.
The transcript was empty (no speech to classify).
Truly cannot determine from the transcript. Used sparingly.
# page 1
curl ".../get-call-logs-api?limit=100&offset=0"
# page 2
curl ".../get-call-logs-api?limit=100&offset=100"
# stop when data.length < limit, or offset >= pagination.totalLast 24 hours of outbound calls for one line:
GTE=$(date -u -v-24H +"%Y-%m-%dT%H:%M:%SZ") # macOS
curl -sS \
-H "Authorization: Bearer $PB_API_KEY" \
"https://api.tryprojectblue.com/get-call-logs-api?pb_line_id=$LINE_ID&answered_at_gte=$GTE"The endpoint doesn't filter by disposition. Filter on the client:
curl -sS -H "Authorization: Bearer $PB_API_KEY" \
"https://api.tryprojectblue.com/get-call-logs-api?limit=100" \
| jq '.data | map(select(.disposition == "answered"))'{
"error": "Invalid or unavailable pb_line_id for this API key user."
}{
"error": "answered_at_gte is not a valid ISO-8601 date"
}Call logs returned
Invalid query parameter (limit/offset/call_log_timestamp/dates/pb_line_id)
Missing or invalid API key
Rate limit exceeded
Internal server error
Use the mediaAttachmentUrl field to send rich media with your messages. The following formats are supported:
Use audioAttachmentUrl to send audio files as voice memos. The following formats are supported:
AI Voice Memos
Set 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.
Voice calling 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.
Your Twilio API key is already created for you. You don't add phone numbers or caller ID — our team provides those.
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.
SMS, iMessage, and MMS stay on the Project Blue API. Only voice calling uses your Twilio key and Twilio's API.
The Project Blue MCP server lets AI assistants (Claude Desktop, Claude Code, Cursor, and any other MCP-compatible client) send iMessages/SMS, look up iMessage availability, and read the user's message history — directly from the editor or chat surface.
https://api.tryprojectblue.com/api/mcp
Paste the MCP server URL into your client's connector settings. Authentication is handled via OAuth 2.1 — your client will open a browser window to app.tryprojectblue.com where you log in and approve the connection. No API key to paste, no tokens to manage.
Open Settings → Connectors → Add custom connector, give it a name (e.g. Project Blue), and paste the server URL. Leave the Advanced / OAuth fields blank — they'll be populated automatically through dynamic client registration. Click Connect and sign in when the consent screen appears.
https://api.tryprojectblue.com/api/mcpclaude mcp add project-blue \
--transport http \
https://api.tryprojectblue.com/api/mcpClaude Code will open a browser to complete the OAuth flow on first use.
Add to ~/.cursor/mcp.json (global) or .cursor/mcp.json (per-project):
{
"mcpServers": {
"project-blue": {
"url": "https://api.tryprojectblue.com/api/mcp"
}
}
}Any client that supports remote MCP over streamable HTTP will work out of the box. For clients that only accept stdio servers, use mcp-remote as an adapter:
{
"mcpServers": {
"project-blue": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://api.tryprojectblue.com/api/mcp"]
}
}
}send_message
Send an iMessage or SMS to a single recipient. Supports media, audio, AI voice memo, and an optional lineId override.
lookup_imessage_availability
Check whether a phone number supports iMessage.
get_lines
List the user's Project Blue sending lines (lineId, devicePhoneNumber, customName).
list_messages
List recent inbound/outbound messages with filters (service, direction, line, date ranges).
get_message
Fetch a single message by its opaque message_handle.
get_call_logs
List the user's outbound dialer call logs with filters for line and answered_at range. Includes call status, disposition, transcript, and recording URL when available.
The MCP server is protected by OAuth 2.1 with Dynamic Client Registration and PKCE. The full flow looks like this:
Client calls the MCP URL without a token. Server responds 401 with a WWW-Authenticate header pointing at the protected-resource metadata.
Client discovers the authorization server (app.tryprojectblue.com), dynamically registers itself, and opens the consent page.
User signs in to Project Blue and clicks Allow. The authorization server mints a new scoped PB API key bound to this connection.
Client receives an access token (pbo_…) and uses it on every MCP request. The token maps back to the user and their scoped key on the server side.
Revoking an MCP connection
You can revoke an MCP connection at any time from Settings → Connected Apps in the Project Blue webapp. Revocation invalidates the token and the scoped API key tied to it — the AI client will be prompted to re-authorize on its next request.
These are emitted automatically for MCP clients that support discovery; you don't need to touch them yourself.
/.well-known/oauth-protected-resource
RFC 9728 — advertises the authorization server for this resource.
/.well-known/oauth-authorization-server
RFC 8414 — served by app.tryprojectblue.com. Lists OAuth endpoints and capabilities.
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.
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

Every webhook event delivers the same payload structure. The direction field indicates whether the message was inbound or outbound.
| Parameter | Type | Required | Description |
|---|---|---|---|
message | string | Required | The text content of the message. |
destination | string | Required | The phone number the message was sent to, in E.164 format. |
receivedAt | string | Required | ISO 8601 timestamp of when the message was received. |
direction | string | Required | Either "inbound" or "outbound", based on message direction. |
messageId | number | Required | Unique numeric identifier for the message. |
guid | string | Required | Globally unique message identifier. |
linePhoneNumber | string | Required | The Project Blue line phone number associated with this message. |
{
"message": "Yes, I'm interested! When can we schedule?",
"destination": "+15551234567",
"receivedAt": "2026-03-04T18:30:00.000Z",
"direction": "inbound",
"messageId": 456,
"guid": "sample-guid-1234",
"linePhoneNumber": "+15559876543"
}{
"message": "Great! Let's book you in for Thursday at 2pm.",
"destination": "+15551234567",
"receivedAt": "2026-03-04T18:31:00.000Z",
"direction": "outbound",
"messageId": 789,
"guid": "sample-guid-5678",
"linePhoneNumber": "+15559876543"
}The API uses standard HTTP status codes. All error responses include a JSON body with an error field describing what went wrong.
{
"error": "Missing or invalid Authorization header"
}Message sent successfully
Invalid request body or missing required fields
Missing or invalid API key
Rate limit exceeded
Internal server error