Loki Messages · API reference

One key. Every iMessage primitive.

Everything below is the live production surface of api.lokimessages.com — the same API that powers our own products. JSON in, JSON out, webhooks back.

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
# create a chat and say hello
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" }'
Response
{ "ok": true }

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" }'
Response
{ "ok": true }
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.

Contacts

Resolve the people behind the handles.

GET /api/contacts/avatar/:handle

Fetch the contact photo for a handle, when one is available — handy for rendering inboxes that look like Messages.