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 or EU phone number — or an email alias — 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.

Unsupported operations

A handful of routes exist for API completeness but return an honest 501 Not Implemented because the iMessage protocol doesn't expose the underlying primitive. Don't build against these — they will not start working with a plan change:

  • PUT /api/chats/:chatId — renaming a chat or changing its icon
  • POST /api/chats/:chatId/participants and DELETE /api/chats/:chatId/participants — adding or removing group members after creation
  • POST /api/chats/:chatId/share_contact_card — sharing a contact card
  • DELETE /api/messages/:messageId — unsending a message
  • PATCH /api/messages/:messageId — editing a sent message
  • POST /api/chats/:chatId/voicememo — sending a voice memo

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. React with one of the six classic tapbacks or with any emoji.

Parameters

type string · required* One of the classic tapbacks: love, like, dislike, laugh, emphasis, question. Use "custom" together with custom_emoji for an emoji tapback. Either type or emoji is required.
emoji string · required* A single emoji for a custom tapback, e.g. "🔥". Shorthand for type "custom" + custom_emoji. Either type or emoji is required.
custom_emoji string · optional The emoji to apply when type is "custom".
remove boolean · optional Set true to take the reaction back instead of adding it. Equivalent to operation "remove".
operation "add" | "remove" · optional Whether to add or remove the reaction. Defaults to "add".
Request
curl -X POST https://api.lokimessages.com/api/messages/$MSG/reactions \
  -H "Authorization: Bearer lok_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "emoji": "🔥" }'
Response
{
  "ok": true,
  "message_id": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
  "reaction": "🔥",
  "action": "add"
}

Returns 202 Accepted — the reaction is dispatched to the network asynchronously.

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.

POST /api/locations/batch

Fetch the latest shared location for several handles in one call. Handles that have shared nothing are omitted from the result.

Parameters

handles string[] · required The handles (phone numbers or Apple ID emails) to look up.
from string · optional The line to read from, when your key spans several.
Request
curl -X POST https://api.lokimessages.com/api/locations/batch \
  -H "Authorization: Bearer lok_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "handles": ["+15559876543", "+15551234567"] }'
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.

Join the wishlist

We're opening up access in waves. Leave your email and we'll reach out the moment it's your turn.

We'll email you the moment it opens up. No spam.