iMessage API documentation

How to send and receive iMessages with Blueping, from your first key to the full endpoint reference.

Quickstart

Get a key on the start page or with npx blueping. You text a six-letter code from your iPhone and the key appears. Your iPhone becomes the contact me, so you can test right away.

curl https://blueping.pages.dev/v1/messages \
  -H "Authorization: Bearer $BLUEPING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to": "me", "text": "Hello from my app"}'
import { Blueping } from "blueping";

const blueping = new Blueping(); // reads BLUEPING_API_KEY

await blueping.send({ to: "me", text: "Hello from my app" });
from blueping import Blueping

blueping = Blueping()  # reads BLUEPING_API_KEY

blueping.send(to="me", text="Hello from my app")

Then share your join link with the people your app should talk to. Each person texts your code once, and from then on you can reply to them.

Authentication

Send your key in the Authorization header on every request. Keys start with bp_live_. Keep them on your server. If one leaks, create a new one from the dashboard or with POST /v1/keys; the old one stops working at once.

Authorization: Bearer bp_live_...

Send a message

POST /v1/messages takes to and text. The person must have joined your app by texting your code, otherwise you get 409 contact_not_joined with the link to share. Add an Idempotency-Key header to retry safely: the same key within 24 hours never sends twice.

{
  "id": "msg_3HkT9cVb2LwQ8sXe5RmN",
  "status": "sent",
  "to": { "id": "ct_Q4nV7wXk2Rb9sL1mTe8P", "handle": "+14155550132" },
  "created_at": "2026-11-04T19:42:09.000Z"
}

Receive replies

Set a webhook URL in the dashboard or with PUT /v1/webhook. Blueping posts every event as JSON and signs it. Answer with a 2xx within 10 seconds. Failed deliveries are retried after 30 seconds, 2 minutes, 10 minutes, 1 hour and 6 hours.

EventWhen
message.receivedA contact who joined your app sent a message
contact.joinedSomeone texted your code
contact.leftThe contact texted LEAVE, or switched to another app (reason left or switched)
contact.opted_outThe contact texted STOP, or you reported an opt-out
{
  "id": "evt_8Lq2mP4xW0aTn7Ks1BvQ",
  "type": "message.received",
  "created_at": "2026-11-04T19:42:07.000Z",
  "data": {
    "message": {
      "id": "msg_9PzR4fYc1KwM6tDq3VxB",
      "from": { "id": "ct_Q4nV7wXk2Rb9sL1mTe8P", "handle": "+14155550132" },
      "text": "Can I move my 7pm to 8?",
      "created_at": "2026-11-04T19:42:07.000Z"
    }
  }
}

Verify the signature

Each request carries Blueping-Signature: t=<unix seconds>,v1=<hex>. v1 is the HMAC-SHA256 of the timestamp, a dot and the raw body, keyed with your webhook secret. Compare in constant time and reject requests older than five minutes.

import crypto from "node:crypto";

// rawBody: the request body as a string, before any JSON parsing.
export function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const expected = Buffer.from(crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex"));
  const received = Buffer.from(parts.v1 ?? "");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  return fresh && expected.length === received.length && crypto.timingSafeEqual(expected, received);
}
import hashlib
import hmac
import time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(part.split("=", 1) for part in header.split(","))
    message = parts["t"].encode() + b"." + raw_body
    expected = hmac.new(secret.encode(), message, hashlib.sha256).hexdigest()
    fresh = abs(time.time() - int(parts["t"])) < 300
    return fresh and hmac.compare_digest(expected, parts.get("v1", ""))

Prefer polling? Call GET /v1/messages?after=<last id> every few seconds.

How people join

The free line is shared by every app on Blueping. A person talks to one app at a time, and picks it by texting that app's code. Your join link opens Messages with your code ready to send, and works from a QR code too. Once someone joins, their thread with the line is a thread with your app until they text another app's code, LEAVE or STOP.

Set your app name with PATCH /v1/me. People see it in the confirmation Blueping sends when they join, so use the name they know you by.

Keywords

Blueping answers these words itself, before anything reaches your app.

WordWhat happens
STOPUnsubscribes the person from every app on the line. You get contact.opted_out
HELPExplains the line and names the app they're talking to
MENULists the apps they joined, with their codes
LEAVELeaves the current app. You get contact.left
LOGINTexts a sign-in link to developers who own an account

Limits

  • 100 messages a day per free account, reset at 00:00 UTC.
  • 5 messages in a row to the same person, then you wait for a reply. A reply resets the count. Messages to me are not limited this way.
  • Text only, up to 2,000 characters. Attachments aren't supported yet.
  • iMessage only: the people you talk to need an iPhone, iPad or Mac.
  • No typing indicators, read receipts or reactions.

Errors

Errors come back as {"error": {"code", "message"}} with extra fields when they help, such as join_url or resets_at.

StatusCodeWhat to do
400invalid_json, text_required, text_too_long, to_requiredFix the request body
401unauthorizedCheck the Authorization header
403account_suspendedWrite to hello@blueping.dev
409contact_not_joinedThe person hasn't texted your code, or is talking to another app. Share join_url
409contact_opted_outThe person texted STOP. They can text your code to come back
429awaiting_replyFive messages in a row. Wait for the person to reply
429daily_limit_reached100 messages today. Try again after resets_at
429line_busyThe shared line reached its volume for today
502line_errorThe line couldn't send. Retry later with the same Idempotency-Key

API reference

Base URL https://blueping.pages.dev. All requests and responses are JSON.

POST/v1/messages

Sends a text to a contact who joined your app. Returns 202 once the line has taken the message.

FieldTypeDescription
tostring"me", a contact id (ct_...), or the phone number or email the person texts from
textstringUp to 2,000 characters
Idempotency-KeyheaderOptional. The same key within 24 hours returns the first answer instead of sending twice
curl https://blueping.pages.dev/v1/messages \
  -H "Authorization: Bearer $BLUEPING_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-shipped" \
  -d '{"to": "+14155550132", "text": "Your order is on its way"}'
{
  "id": "msg_3HkT9cVb2LwQ8sXe5RmN",
  "status": "sent",
  "to": { "id": "ct_Q4nV7wXk2Rb9sL1mTe8P", "handle": "+14155550132" },
  "created_at": "2026-11-04T19:42:09.000Z"
}

GET/v1/messages

Lists messages in and out, oldest first. Without after, returns the latest ones.

FieldTypeDescription
afterstringA message id. Returns what came after it, so you can poll
limitnumber1 to 200, default 50
curl "https://blueping.pages.dev/v1/messages?limit=20" \
  -H "Authorization: Bearer $BLUEPING_API_KEY"
{
  "data": [
    {
      "id": "msg_9PzR4fYc1KwM6tDq3VxB",
      "direction": "in",
      "contact": { "id": "ct_Q4nV7wXk2Rb9sL1mTe8P", "handle": "+14155550132" },
      "text": "Can I move my 7pm to 8?",
      "status": "received",
      "created_at": "2026-11-04T19:42:07.000Z"
    }
  ],
  "next_after": "msg_9PzR4fYc1KwM6tDq3VxB"
}

GET/v1/contacts

Lists the people who joined your app, and how many messages you can send each of them before they reply.

curl https://blueping.pages.dev/v1/contacts -H "Authorization: Bearer $BLUEPING_API_KEY"
{
  "data": [
    {
      "id": "ct_Q4nV7wXk2Rb9sL1mTe8P",
      "handle": "+14155550132",
      "active": true,
      "opted_out": false,
      "remaining_before_reply": 4,
      "joined_at": "2026-11-04T19:40:51.000Z",
      "last_message_at": "2026-11-04T19:42:07.000Z"
    }
  ]
}

POST/v1/contacts/{id}/opt-out

Unsubscribes a contact from every app on the line. Use it when someone asks to stop in words Blueping can't recognize, such as "please don't text me again".

curl -X POST https://blueping.pages.dev/v1/contacts/ct_Q4nV7wXk2Rb9sL1mTe8P/opt-out \
  -H "Authorization: Bearer $BLUEPING_API_KEY"

POST/v1/invites

Returns your join code, your join link and a ready-made sms: link.

curl -X POST https://blueping.pages.dev/v1/invites -H "Authorization: Bearer $BLUEPING_API_KEY"
{
  "code": "K7Q2MX",
  "url": "https://blueping.pages.dev/j/K7Q2MX",
  "sms_url": "sms:hello@blueping.dev?&body=K7Q2MX",
  "line": "hello@blueping.dev"
}

GET/v1/me

Your account, today's usage, your join link and your webhook. PATCH /v1/me with {"name": "..."} sets the app name people see.

curl https://blueping.pages.dev/v1/me -H "Authorization: Bearer $BLUEPING_API_KEY"

PUT/v1/webhook

Sets the URL that receives events and returns its signing secret. DELETE /v1/webhook removes it.

FieldTypeDescription
urlstringAn https URL
curl -X PUT https://blueping.pages.dev/v1/webhook \
  -H "Authorization: Bearer $BLUEPING_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/blueping"}'
{ "url": "https://example.com/blueping", "secret": "whsec_4f0c..." }

POST/v1/keys

Creates a new API key and revokes the old ones. The new key is returned once.

curl -X POST https://blueping.pages.dev/v1/keys -H "Authorization: Bearer $BLUEPING_API_KEY"

SDKs and tools

The Node.js and Python SDKs wrap every endpoint, retry safely and verify webhooks. The CLI gets you a key from the terminal, and the MCP server lets AI assistants send and read messages.

npm install blueping
pip install blueping
npx blueping
npx blueping-mcp