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.
| Event | When |
|---|---|
message.received | A contact who joined your app sent a message |
contact.joined | Someone texted your code |
contact.left | The contact texted LEAVE, or switched to another app (reason left or switched) |
contact.opted_out | The 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.
| Word | What happens |
|---|---|
| STOP | Unsubscribes the person from every app on the line. You get contact.opted_out |
| HELP | Explains the line and names the app they're talking to |
| MENU | Lists the apps they joined, with their codes |
| LEAVE | Leaves the current app. You get contact.left |
| LOGIN | Texts 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
meare 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.
| Status | Code | What to do |
|---|---|---|
| 400 | invalid_json, text_required, text_too_long, to_required | Fix the request body |
| 401 | unauthorized | Check the Authorization header |
| 403 | account_suspended | Write to hello@blueping.dev |
| 409 | contact_not_joined | The person hasn't texted your code, or is talking to another app. Share join_url |
| 409 | contact_opted_out | The person texted STOP. They can text your code to come back |
| 429 | awaiting_reply | Five messages in a row. Wait for the person to reply |
| 429 | daily_limit_reached | 100 messages today. Try again after resets_at |
| 429 | line_busy | The shared line reached its volume for today |
| 502 | line_error | The 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.
| Field | Type | Description |
|---|---|---|
to | string | "me", a contact id (ct_...), or the phone number or email the person texts from |
text | string | Up to 2,000 characters |
Idempotency-Key | header | Optional. 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.
| Field | Type | Description |
|---|---|---|
after | string | A message id. Returns what came after it, so you can poll |
limit | number | 1 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.
| Field | Type | Description |
|---|---|---|
url | string | An 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