Project Blue
Project Blue
API Reference
  • Getting Started
  • Introduction
    Authentication
  • Messages
  • Send Message
    CRM Integration
    List Messages
    Get Message
  • Contacts
  • External Contacts
  • Calls
  • Call Logs
    FaceTime Audio
    Calling
  • Lookups
  • Check iMessage
    Get Lines
  • Integrations
  • MCP Server
    Webhooks
  • Reference
  • Supported Media
    Voice Memos & Audio
    Error Handling
API Documentation
v1.0
Project Blue
REST API

Project Blue API

Send iMessages and SMS directly from your application. Integrate messaging into your CRM, workflows, or custom tools with a single API call.

Base URL

https://api.tryprojectblue.com

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.

API Keys settings in the Project Blue dashboard
HTTP Header
Authorization: Bearer YOUR_API_KEY

Keep your API keys secure

Never expose your API key in client-side code or public repositories. Always make API calls from your server.

Send Message

POST
/send-api-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.

Request Body
ParameterTypeRequiredDescription
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.

firstName

string

Optional

First name to persist on the auto-created Project Blue contact for this recipient. Applies to accounts without a connected CRM (external/API source).

lastName

string

Optional

Last name to persist on the auto-created Project Blue contact for this recipient. Applies to accounts without a connected CRM (external/API source).

email

string

Optional

Email address to persist on the auto-created Project Blue contact for this recipient. Applies to accounts without a connected CRM (external/API source).

customFields

object

Optional

Arbitrary JSON metadata (e.g. order IDs, links) persisted on the auto-created contact and shown in the contact details panel in the Project Blue app. Plain object only; max 10KB serialized.

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.

Example Request
cURL
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"
  }'
Example with Media Attachment
cURL
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"
  }'
Example with AI Voice Memo
cURL
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
  }'
Example with Line Override
cURL
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 Response
JSON — 200 OK
{
  "success": true,
  "message": "Message added to queue",
  "messageType": "iMessage",
  "phone": "+15551234567",
  "devicePhoneNumber": "+15559876543",
  "mediaAttachmentUrl": null,
  "audioAttachmentUrl": null
}
Error Response (Invalid Line Override)
JSON — 400 Bad Request
{
  "error": "Invalid lineId. Expected a UUID v4 token returned from GET /get-lines."
}

CRM Integration

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.

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.

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.

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.

cURL
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 iMessage Availability

POST
/api-check-imessage-availability

Check whether a phone number is reachable via iMessage before sending. Useful for routing logic or pre-qualifying contacts.

Request Body
ParameterTypeRequiredDescription
phone

string

Required

The phone number to check. Accepts many formats — we normalize to E.164.

Example Request
cURL
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"
  }'
Success Response
JSON — 200 OK
{
  "normalizedPhone": "+15551234567",
  "isIMessageAvailable": true
}
Error Responses
JSON — 400 Bad Request
{
  "error": "Invalid phone number format (Not a valid phone number). Please use format +12345678901"
}
200

Phone checked successfully

400

Invalid phone number format

401

Missing or invalid API key

429

Rate limit exceeded

500

Internal server error

Get Lines

GET
/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.

Example Request
cURL
curl -X GET https://api.tryprojectblue.com/get-lines \
  -H "Authorization: Bearer YOUR_API_KEY"
Success Response
JSON — 200 OK
[
  {
    "lineId": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5",
    "devicePhoneNumber": "+15559876543",
    "customName": "Main Line"
  },
  {
    "lineId": "9c2e4a1b-d3f8-4e1c-a2b4-c5d6e7f8a9b0",
    "devicePhoneNumber": "+15557654321",
    "customName": "Secondary Line"
  }
]
Error Response
JSON — 401 Unauthorized
{
  "error": "Missing or invalid Authorization header"
}

List Messages

GET
/get-messages-api

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.

Query Parameters
ParameterTypeRequiredDescription
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.

Example Request
cURL
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"
Success Response
JSON — 200 OK
{
  "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 Response
JSON — 400 Bad Request
{
  "error": "direction=inbound cannot be combined with to_number"
}

Get Message

GET
/get-message-api/:message_handle

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.

Path Parameters
ParameterTypeRequiredDescription
message_handle

string

Required

Opaque handle starting with pbm_, returned by /get-messages-api.

Example Request
cURL
curl https://api.tryprojectblue.com/get-message-api/pbm_outk6dawyUgbl-EjXCZk-g5mZnmwmSbilbaX \
  -H "Authorization: Bearer YOUR_API_KEY"
Success Response
JSON — 200 OK
{
  "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 Responses
JSON — 404 Not Found
{
  "error": "Message not found"
}
JSON — 400 Bad Request
{
  "error": "Invalid or missing message_handle"
}

External Contacts

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.

For accounts without a connected CRM

If HighLevel or HubSpot is connected, contacts live in your CRM instead (see CRM Integration). External contacts apply to external/API-source accounts.

Create a Contact
POST
/create-external-contact

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.

Request Body
ParameterTypeRequiredDescription
phone

string

Required

The contact's phone number. Accepts many formats — normalized to E.164.

firstName

string

Optional

The contact's first name.

lastName

string

Optional

The contact's last name.

email

string

Optional

The contact's email address.

customFields

object

Optional

Arbitrary JSON metadata (e.g. order IDs, links) shown in the contact details panel. Plain object only; max 10KB serialized.

Example Request
cURL
curl -X POST https://api.tryprojectblue.com/create-external-contact \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+15551234567",
    "firstName": "Jamie",
    "lastName": "Rivera",
    "email": "jamie@example.com",
    "customFields": {
      "orderId": "ord_18342",
      "plan": "pro"
    }
  }'
Success Response
JSON — 200 OK
{
  "status": "OK",
  "contact": {
    "id": "ctc_9f2a1b3c4d5e",
    "firstName": "Jamie",
    "lastName": "Rivera",
    "phone": "+15551234567",
    "email": "jamie@example.com",
    "customFields": {
      "orderId": "ord_18342",
      "plan": "pro"
    },
    "note": null
  }
}
List Contacts
GET
/get-external-contacts

Returns the authenticated user's external contacts, newest first, with pagination. Pass phone to look up a single contact by number.

Query Parameters
ParameterTypeRequiredDescription
limit

integer (1–100)

Optional

Maximum number of contacts to return. Defaults to 100.

offset

integer (≥ 0)

Optional

Pagination offset. Defaults to 0.

phone

string

Optional

Look up a single contact by phone number (exact match after normalization to E.164).

Example Request
cURL
curl -G https://api.tryprojectblue.com/get-external-contacts \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "limit=25"
Success Response
JSON — 200 OK
{
  "status": "OK",
  "data": [
    {
      "id": "ctc_9f2a1b3c4d5e",
      "firstName": "Jamie",
      "lastName": "Rivera",
      "phone": "+15551234567",
      "email": "jamie@example.com",
      "customFields": {
        "orderId": "ord_18342",
        "plan": "pro"
      },
      "note": null
    }
  ],
  "pagination": { "limit": 25, "offset": 0, "total": 138 }
}
Update a Contact
POST
/update-external-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.

Request Body
ParameterTypeRequiredDescription
contactId

string

Required

The contact id, as returned by the create or list endpoints.

firstName

string

Optional

New first name for the contact.

lastName

string

Optional

New last name for the contact.

email

string

Optional

New email address for the contact.

note

string

Optional

Free-form note stored on the contact.

customFields

object

Optional

Replaces the entire stored customFields object — fetch the current contact and merge client-side to preserve existing keys. Plain object only; max 10KB serialized.

customFields is replaced, not merged

Sending 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.

Example Request
cURL
curl -X POST https://api.tryprojectblue.com/update-external-contact \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contactId": "ctc_9f2a1b3c4d5e",
    "note": "VIP customer — prefers texts over calls",
    "customFields": {
      "orderId": "ord_18342",
      "plan": "enterprise"
    }
  }'
Success Response
JSON — 200 OK
{
  "status": "OK",
  "contact": {
    "id": "ctc_9f2a1b3c4d5e",
    "firstName": "Jamie",
    "lastName": "Rivera",
    "phone": "+15551234567",
    "email": "jamie@example.com",
    "customFields": {
      "orderId": "ord_18342",
      "plan": "enterprise"
    },
    "note": "VIP customer — prefers texts over calls"
  }
}
Error Responses
JSON — 404 Not Found
{
  "error": "Contact not found"
}
JSON — 400 Bad Request
{
  "error": "customFields exceeds the 10KB limit"
}

Call Logs

GET
/get-call-logs-api

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).

Query Parameters
ParameterTypeRequiredDescription
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.

Example Request
cURL
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"
Success Response
JSON — 200 OK
{
  "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 }
}
CallLog Object
ParameterTypeRequiredDescription
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.

Dispositions

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.

answered

A human answered and spoke (any clear human speech that isn't a voicemail greeting).

voicemail

The call went to voicemail (voicemail greeting, beep, or 'leave a message' prompt detected).

busy

The line was busy.

no_answer

No one answered — just ringing or silence.

wrong_number

The person on the other end indicated this is the wrong number.

not_interested

The person explicitly declined or showed no interest.

callback_requested

The person asked to be called back at a later time.

meeting_scheduled

A meeting or appointment was scheduled on the call.

information_provided

Information was exchanged but no clear next step was set.

no_speech

The transcript was empty (no speech to classify).

unknown

Truly cannot determine from the transcript. Used sparingly.

Pagination
bash
# 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.total
Filtering Examples

Last 24 hours of outbound calls for one line:

bash
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:

bash
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 Responses
JSON — 400 Bad Request
{
  "error": "Invalid or unavailable pb_line_id for this API key user."
}
JSON — 400 Bad Request
{
  "error": "answered_at_gte is not a valid ISO-8601 date"
}
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

Supported Media Types

Use the mediaAttachmentUrl field to send rich media with your messages. The following formats are supported:

Images
JPEG / JPG
PNG
GIF
WebP
Videos
MP4
MOV
AVI
MKV
FLV
WebM
Contact Cards
VCF / vCard

Voice Memos & Audio

Use audioAttachmentUrl to send audio files as voice memos. The following formats are supported:

M4A
MP3
WAV
OGG
WebM
CAF

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.

FaceTime Audio

FaceTime-enabled accounts only

These endpoints are available only on API accounts with FaceTime Audio enabled. Calls from accounts without the feature return 403 with error code 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.

Start a Call
POST
/start-facetime-call-api
Request Body
ParameterTypeRequiredDescription
pb_line_id

string (UUID)

Required

FaceTime-enabled line to call from. UUID returned from GET /get-lines.

phone_number

string (E.164)

Required

Destination phone number. Flexible formats accepted; normalized to E.164.

Example Request
cURL
curl -X POST https://api.tryprojectblue.com/start-facetime-call-api \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "pb_line_id": "a3f8c2d1-b4e9-4f2a-c8d3-e1f0a2b3c4d5",
    "phone_number": "+15551234567"
  }'
Success Response
JSON — 200 OK
{
  "status": "OK",
  "call_uuid": "ftc_8a2b1c3d4e5f6g7h",
  "agora": {
    "appId": "your-agora-app-id",
    "channelName": "facetime-channel-abc123",
    "token": "agora-rtc-token...",
    "uid": 123456
  }
}
Joining 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.

JavaScript
import AgoraRTC from "agora-rtc-sdk-ng";

const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
await client.join(
  res.agora.appId,
  res.agora.channelName,
  res.agora.token,
  res.agora.uid,
);
const mic = await AgoraRTC.createMicrophoneAudioTrack();
await client.publish([mic]);
client.on("user-published", async (user, mediaType) => {
  await client.subscribe(user, mediaType);
  if (mediaType === "audio") user.audioTrack.play();
});
Call Status
GET
/get-facetime-call-status-api

Status is driven by call lifecycle events. Poll this endpoint to track ringing answered ended.

Query Parameters
ParameterTypeRequiredDescription
call_uuid

string

Required

The call_uuid returned by POST /start-facetime-call-api.

Example Request
cURL
curl -G https://api.tryprojectblue.com/get-facetime-call-status-api \
  -H "Authorization: Bearer YOUR_API_KEY" \
  --data-urlencode "call_uuid=ftc_8a2b1c3d4e5f6g7h"
Success Response
JSON — 200 OK
{
  "status": "OK",
  "call_uuid": "ftc_8a2b1c3d4e5f6g7h",
  "call_status": "answered",
  "direction": "outbound",
  "address": "+15551234567",
  "answered_at": "2026-07-18T18:04:12.000Z",
  "ended_at": null
}

Possible call_status values:

initiated
ringing
answered
ended
declined
no_answer
failed
End Call
POST
/end-facetime-call-api
Request Body
ParameterTypeRequiredDescription
call_uuid

string

Required

The call_uuid returned by POST /start-facetime-call-api.

Example Request
cURL
curl -X POST https://api.tryprojectblue.com/end-facetime-call-api \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "call_uuid": "ftc_8a2b1c3d4e5f6g7h"
  }'
Success Response
JSON — 200 OK
{
  "status": "OK"
}

Daily dial limit

Each account may dial up to 40 unique destinations per calendar day (America/Los_Angeles). Redials to a number already called that day do not count. Exceeding the limit returns 429 with error_code: FACETIME_DAILY_LIMIT_REACHED plus uniqueDestinationsToday and limit fields.

Status Codes
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 the FaceTime Audio section 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.

Twilio API key in Project Blue Settings → API Keys

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.

Model Context Protocol (MCP)

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.

Server URL

https://api.tryprojectblue.com/api/mcp

Quick Start

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.

Setup by Client
Claude Desktop

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.

MCP Server URL
https://api.tryprojectblue.com/api/mcp
Claude Code
Terminal
claude mcp add project-blue \
  --transport http \
  https://api.tryprojectblue.com/api/mcp

Claude Code will open a browser to complete the OAuth flow on first use.

Cursor

Add to ~/.cursor/mcp.json (global) or .cursor/mcp.json (per-project):

JSON — .cursor/mcp.json
{
  "mcpServers": {
    "project-blue": {
      "url": "https://api.tryprojectblue.com/api/mcp"
    }
  }
}
Other MCP Clients

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:

JSON — mcp-remote fallback
{
  "mcpServers": {
    "project-blue": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://api.tryprojectblue.com/api/mcp"]
    }
  }
}
Available Tools
Tool
Type
Description

send_message

write

Send an iMessage or SMS to a single recipient. Supports media, audio, AI voice memo, and an optional lineId override.

lookup_imessage_availability

read

Check whether a phone number supports iMessage.

get_lines

read

List the user's Project Blue sending lines (lineId, devicePhoneNumber, customName).

list_messages

read

List recent inbound/outbound messages with filters (service, direction, line, date ranges).

get_message

read

Fetch a single message by its opaque message_handle.

get_call_logs

read

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.

start_facetime_call

write

Place a FaceTime Audio call from a FaceTime-enabled line. Returns call_uuid plus Agora WebRTC credentials the caller must use to join the call audio. Requires FaceTime Audio on the account.

get_facetime_call_status

read

Poll a FaceTime call's live status (initiated, ringing, answered, ended, declined, no_answer, failed) by call_uuid.

end_facetime_call

write

Hang up an in-progress FaceTime call by call_uuid.

create_external_contact

write

Create (or upsert by phone number) an external contact with name, email, and custom JSON metadata. For accounts without a connected CRM.

get_external_contacts

read

List the user's external contacts (newest first) with pagination, or look one up by phone number.

update_external_contact

write

Update an external contact's name, email, note, or customFields by contact id. customFields is replaced wholesale, not merged.

FaceTime tools require a FaceTime-enabled account

start_facetime_call places a real call and returns Agora WebRTC credentials — your MCP client or application must join the Agora channel to carry the audio. Accounts without FaceTime Audio receive a 403 with error_code FACETIME_NOT_ENABLED; contact sales or support to enable the feature.

Authentication

The MCP server is protected by OAuth 2.1 with Dynamic Client Registration and PKCE. The full flow looks like this:

  1. Client calls the MCP URL without a token. Server responds 401 with a WWW-Authenticate header pointing at the protected-resource metadata.

  2. Client discovers the authorization server (app.tryprojectblue.com), dynamically registers itself, and opens the consent page.

  3. User signs in to Project Blue and clicks Allow. The authorization server mints a new scoped PB API key bound to this connection.

  4. 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.

Discovery Endpoints

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

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

Webhook configuration in the Project Blue dashboard
Webhook Payload

Every webhook event delivers the same payload structure. The direction field indicates whether the message was inbound or outbound.

ParameterTypeRequiredDescription
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.

Example — Inbound
JSON — Inbound Webhook
{
  "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"
}
Example — Outbound
JSON — Outbound Webhook
{
  "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"
}

Error Handling

The API uses standard HTTP status codes. All error responses include a JSON body with an error field describing what went wrong.

JSON — 401 Unauthorized
{
  "error": "Missing or invalid Authorization header"
}
200

Message sent successfully

400

Invalid request body or missing required fields

401

Missing or invalid API key

429

Rate limit exceeded

500

Internal server error

© 2026 Project Blue Services LLC

WebsiteContact