Introduction
The Loki Messages API turns a real US phone number into a
programmable iMessage conversation surface. The base URL for
every call is https://api.lokimessages.com; all
requests and responses are JSON.
The mental model is small: chats contain
messages, messages are made of text and media
parts, and everything that happens inbound —
replies, reactions, read receipts, poll votes — reaches your
backend as a webhook.
quickstart — first message in one call
curl -X POST https://api.lokimessages.com/api/chats \
-H "Authorization: Bearer lok_live_…" \
-H "Content-Type: application/json" \
-d '{"to":["+15559876543"],"message":{"parts":[{"type":"text","value":"hi 👋"}]}}'
Authentication
Every request is authenticated with a workspace-scoped API key
in the Authorization header:
Bearer lok_live_…. Create and revoke keys in the
dashboard. Keys carry the scope of your workspace's lines — a
key can only send from numbers it owns.
Treat keys like passwords: server-side only, never in client
code, rotated if exposed.
Errors
Errors are JSON with a single error message and a
conventional status code: 400 for invalid input,
401 for a missing or bad key, 403 when
a key touches a line it doesn't own, 404 for
missing resources. Primitives the iMessage protocol doesn't
expose (editing sent messages, changing group membership) return
an honest 501 rather than pretending.
error shape
{ "error": "'to' is required" }
Webhooks
Point a webhook URL at your backend in the dashboard and every
inbound event arrives as structured JSON, typically in under a
second: new messages, reactions, read receipts, and poll votes.
Sender, chat, and attachments are already resolved — media
comes with a download URL, no second round-trip.
message.received
{
"event": "message.received",
"chat_id": "cht_a1b2c3d4e5f6",
"message": {
"id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"direction": "inbound",
"sender_handle": { "handle": "+15559876543" },
"parts": [ { "type": "text", "value": "great, walking over now" } ]
}
}
Respond with a 2xx quickly and process async — slow webhook
handlers are the most common integration bug we see.
Chats
A chat is a conversation — direct or group — on one of your lines. Chats are created implicitly when someone texts you, or explicitly when you start one.
GET /api/chats
List chats on your workspace's lines, most recent first. Paginated with an opaque cursor.
Parameters
| cursor | query · optional | Cursor from a previous response's next_cursor. |
| limit | query · optional | Page size. Sensible default; capped server-side. |
Request
curl https://api.lokimessages.com/api/chats \
-H "Authorization: Bearer lok_live_…"
Response
{
"chats": [
{
"id": "cht_a1b2c3d4e5f6",
"display_name": null,
"service": "iMessage",
"is_group": false,
"handles": [
{ "id": "+15559876543", "handle": "+15559876543", "is_me": false }
]
}
],
"next_cursor": "MTA0Mg=="
} POST /api/chats
Create a chat and send its first message in one call. Pass one recipient for a direct chat or several for a group. The first message is required — that's what materializes the thread on the network.
Parameters
| from | string · optional | The line (your number) to send from. Defaults to your workspace's line when the API key is scoped to one. |
| to | string[] · required | Recipient handles — phone numbers or Apple ID emails. More than one creates a group. |
| message.parts | part[] · required | Message content. Text parts: { "type": "text", "value": "…" }. Media parts: { "type": "media", "attachment_id": "…" } or { "type": "media", "url": "…" }. |
| message.service | "iMessage" | "SMS" · optional | Transport for the chat. Defaults to iMessage. |
| auto_type | boolean · optional | Show a natural typing indicator before the message lands. |
Request
curl -X POST https://api.lokimessages.com/api/chats \
-H "Authorization: Bearer lok_live_…" \
-H "Content-Type: application/json" \
-d '{
"to": ["+15559876543"],
"message": { "parts": [ { "type": "text", "value": "Hello! How are you?" } ] }
}' Response
{
"chat": {
"id": "cht_a1b2c3d4e5f6",
"service": "iMessage",
"is_group": false,
"handles": [ { "id": "+15559876543", "handle": "+15559876543", "is_me": false } ],
"message": {
"id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"service": "iMessage",
"parts": [ { "type": "text", "value": "Hello! How are you?" } ]
}
}
} GET /api/chats/:chatId
Fetch a single chat by ID, including its participant handles and service.
Messages
Messages are parts-based: a message is an ordered list of text and media parts. Everything you send and everything you receive uses the same shape.
POST /api/chats/:chatId/messages
Send a message into an existing chat. Combine text and media parts in one message; reference uploaded attachments by ID or pass a public URL.
Parameters
| message.parts | part[] · required | Ordered text and media parts. |
| message.reply_to | string · optional | Message ID to reply to — creates a native iMessage thread. |
| auto_type | boolean · optional | Show a typing indicator first, scaled to message length. |
Request
curl -X POST https://api.lokimessages.com/api/chats/$CHAT/messages \
-H "Authorization: Bearer lok_live_…" \
-H "Content-Type: application/json" \
-d '{
"message": {
"parts": [
{ "type": "text", "value": "Here is the menu 👇" },
{ "type": "media", "attachment_id": "att_8f2k…" }
]
}
}' Response
{
"message": {
"id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
"direction": "outbound",
"service": "iMessage",
"sent_at": "2026-06-11T18:30:00Z",
"delivered_at": null,
"read_at": null
}
} GET /api/chats/:chatId/messages
Page through a chat's history, newest first, with the same cursor pagination as chats. Each message carries direction, sender handle, parts, and delivery/read timestamps.
GET /api/messages/:messageId
Fetch one message by its GUID — useful for resolving webhook events into full objects.
GET /api/messages/:messageId/thread
Read a message's full reply thread as one resource: the root, every reply, in order.
Reactions
Tapbacks, programmable. React with the six classics or any emoji, and receive every reaction your users send as a webhook event.
POST /api/messages/:messageId/reactions
Add or remove a reaction on a message.
Parameters
| type | string · required | love, like, dislike, laugh, emphasis, question — or any emoji for a custom reaction. |
| remove | boolean · optional | Set true to take a reaction back. |
Request
curl -X POST https://api.lokimessages.com/api/messages/$MSG/reactions \
-H "Authorization: Bearer lok_live_…" \
-H "Content-Type: application/json" \
-d '{ "type": "love" }' Typing & read receipts
The small signals that make a thread feel alive. Show the dots while your system thinks; mark chats read so your number behaves like a person, not a void.
POST /api/chats/:chatId/typing
Show the typing indicator in a chat. It clears automatically when you send, or explicitly with the DELETE below.
DELETE /api/chats/:chatId/typing
Hide the typing indicator without sending.
POST /api/chats/:chatId/read
Mark the chat read — the sender sees their read receipt, exactly as if a person opened the thread.
GET /api/chats/:chatId/read-status
Per-participant read state for a chat.
GET /api/messages/:messageId/read-status
Delivery and read state for one message.
Polls
Native iMessage polls — the cleanest structured input a text thread can collect. Votes come back as webhook events as they happen.
POST /api/chats/:chatId/polls
Create a poll in a chat with 2–10 options.
Parameters
| title | string · required | The question. |
| options | string[] · required | 2 to 10 non-empty choices. |
Request
curl -X POST https://api.lokimessages.com/api/chats/$CHAT/polls \
-H "Authorization: Bearer lok_live_…" \
-H "Content-Type: application/json" \
-d '{ "title": "Run tomorrow?", "options": ["6am", "7am", "rest day"] }' Response
{ "ok": true, "poll": { "id": "pol_3c9d…", "title": "Run tomorrow?" } } Attachments
Upload once, send anywhere. Create an attachment, push its bytes, then reference it from any number of messages as a media part. Inbound media arrives in webhooks with a download URL.
POST /api/attachments/upload
Register an attachment (filename + content type) and receive its ID and an upload target.
Request
curl -X POST https://api.lokimessages.com/api/attachments/upload \
-H "Authorization: Bearer lok_live_…" \
-H "Content-Type: application/json" \
-d '{ "filename": "menu.pdf", "content_type": "application/pdf" }' Response
{ "attachment": { "id": "att_8f2k…", "filename": "menu.pdf" } } PUT /api/attachments/:id/content
Upload the raw bytes for an attachment you registered.
GET /api/attachments/:id
Fetch attachment metadata.
GET /api/attachments/:id/download
Download an attachment's bytes — including media your users sent you.
DELETE /api/attachments/:id
Delete an attachment you no longer need.
Locations
The primitive behind every "where are you?" flow: ask a handle for their location and read back what they share.
POST /api/locations/request
Ask a handle to share their location. They get the native share prompt; what they share becomes readable below.
Parameters
| handle | string · required | Phone number or Apple ID email to ask. |
| from | string · optional | The line to ask from, when your key spans several. |
Request
curl -X POST https://api.lokimessages.com/api/locations/request \
-H "Authorization: Bearer lok_live_…" \
-H "Content-Type: application/json" \
-d '{ "handle": "+15559876543" }' GET /api/locations
List the latest shared locations across your handles.
GET /api/locations/:handle
The most recent location a specific handle has shared with you.