Overview
The 247Rep API lets you integrate WhatsApp and email messaging directly into your own products and workflows. It is organised around REST: resources are accessed at predictable URLs, requests use standard HTTP verbs, and every response is JSON.
All requests must be made over HTTPS and authenticated with an API key.
- JSON request and response bodies
- Bearer token authentication with scoped, revocable API keys
- Separate test and live environments
- Cursor-based pagination on all list endpoints
- Per-endpoint rate limiting with standard
X-RateLimit-*headers - Real-time webhooks for inbound and delivery events
Base URL
All API endpoints are relative to the following base URL:
https://api.247rep.app/v1For example, to send a WhatsApp message you would issue a POST request to https://api.247rep.app/v1/messages/send.
Test & live modes
Every workspace has two independent API environments, identified by the key prefix. The environment is inferred from the key you authenticate with — there is no separate header.
| Prefix | Mode | Behaviour |
|---|---|---|
rep_live_… | Live | Sends real messages and deducts real credits. |
rep_test_… | Test | Validates and authenticates exactly like live, but never sends a real message and never deducts credits. Mutating endpoints return a mock response with test: true. |
send and broadcast calls return a synthetic messageId/broadcastId and creditsUsed: 0.API keys
Authenticate every request with a secret API key. Create and revoke keys in your dashboard under Developer API. Each key is scoped to a single workspace and channel, and can be generated for either the test or live environment.
Keys are shown in full only once at creation. 247Rep stores only a SHA-256 hash, so a lost key cannot be recovered — generate a new one instead.
Authorization header
Pass your key as a bearer token in the Authorization header on every request:
Authorization: Bearer rep_live_your_key_hereA request with a missing, malformed, or revoked key returns 401 Unauthorized:
{
"code": "INVALID_KEY",
"message": "Invalid API key"
}Quick start
Send your first WhatsApp message in one request. Replace the key and recipient with your own.
curl -X POST https://api.247rep.app/v1/messages/send \
-H "Authorization: Bearer rep_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"to": "+2348012345678",
"type": "text",
"text": { "body": "Hello from 247Rep!" }
}'const res = await fetch("https://api.247rep.app/v1/messages/send", {
method: "POST",
headers: {
Authorization: "Bearer rep_live_your_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
to: "+2348012345678",
type: "text",
text: { body: "Hello from 247Rep!" },
}),
});
const data = await res.json();import requests
res = requests.post(
"https://api.247rep.app/v1/messages/send",
headers={"Authorization": "Bearer rep_live_your_key"},
json={
"to": "+2348012345678",
"type": "text",
"text": {"body": "Hello from 247Rep!"},
},
)
data = res.json()Pagination
List endpoints are cursor-paginated. Pass limit (default 50, max 100) and a cursor to fetch the next page. Each response includes hasMore and a nextCursor — pass that value as the cursor on the following request.
Parameters
limitcursor{
"data": [ /* ... items ... */ ],
"hasMore": true,
"nextCursor": "cm5a1b2c3d4e5f6g7h8i9j0k"
}Rate limits
Rate limits are applied per API key and vary by endpoint category. A global burst limit of 20 requests/second also applies across all endpoints.
| Category | Limit | Applies to |
|---|---|---|
| Send | 100 / min | Sending messages, closing conversations, deleting contacts |
| Broadcast | 10 / min | Broadcast sends |
| Contacts (write) | 200 / min | Creating / upserting contacts |
| Read | 300 / min | All GET endpoints |
| Webhooks | 20 / min | Creating / deleting webhooks |
| Burst | 20 / sec | All endpoints (global) |
Every response includes the current limit state:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
X-RateLimit-Reset: 1718745600When a limit is exceeded the API returns 429 Too Many Requests with a Retry-After header (seconds):
{
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Please retry after the specified time."
}Idempotency
Network errors happen. If your server retries a send and we already processed the first attempt, you do not want a second message going out. Every write endpoint (anything that sends a message, broadcasts, closes a conversation, creates or deletes a webhook, etc.) accepts an Idempotency-Key header. Set it to a unique value per logical request (a UUID works well) and we will return the original response on any retry within 24 hours.
curl -X POST https://api.247rep.app/v1/messages/send \
-H "Authorization: Bearer rep_live_your_key" \
-H "Idempotency-Key: 8c3d2f60-4e6e-4b4e-9a5d-1d92f1d8d8a1" \
-H "Content-Type: application/json" \
-d '{
"to": "+2348012345678",
"type": "text",
"text": { "body": "Hello" }
}'How it behaves
- First request with a given key runs normally and we cache the response for 24 hours.
- Any retry within 24 hours that reuses the same key replays the original response with header
Idempotent-Replayed: true. No duplicate send, no duplicate credit charge. - If a retry sends a different body with the same key, we return
409 Conflictwith codeIDEMPOTENCY_CONFLICT. Use a fresh key for a different payload. - If two requests with the same key arrive at the same time, the second one gets
409with codeIDEMPOTENCY_IN_PROGRESS. Wait a moment and retry.
Credits & costs
Sending messages consumes credits from your workspace balance. The cost depends on the channel and the message type. Read endpoints are free. Inbound messages are free.
| Channel | Action | Cost |
|---|---|---|
| WhatsApp / Telegram | Text message | 1 credit |
| WhatsApp / Telegram | Image | 2 credits |
| WhatsApp / Telegram | Video | 5 credits |
| WhatsApp / Telegram | Document or audio | 2 credits |
| Template message | 2 credits | |
| Any email (with or without attachments) | 1 credit | |
| Broadcast | Per recipient (same as the underlying message type) |
Send responses include creditsUsed and the remaining creditsRemaining balance. If your balance is too low the API returns 402 Payment Required with code INSUFFICIENT_CREDITS. In test mode creditsUsed is always 0.
Errors
247Rep uses conventional HTTP status codes. 2xx indicates success, 4xx a problem with the request, and 5xx a server error. Every error body has the same shape:
{
"code": "INSUFFICIENT_CREDITS",
"message": "Insufficient credits. Required: 2, available: 0",
"docs": "https://docs.247rep.com/errors#INSUFFICIENT_CREDITS"
}| Code | HTTP | Meaning |
|---|---|---|
INVALID_AUTH | 401 | Missing or malformed Authorization header. |
INVALID_KEY | 401 | The API key does not exist or has the wrong format. |
REVOKED_KEY | 401 | The API key has been revoked. |
FORBIDDEN | 403 | Key not authorized for this resource (e.g. email endpoints require an email channel key). |
INSUFFICIENT_CREDITS | 402 | Workspace balance is too low for this action. |
VALIDATION_ERROR | 400 | The request body failed validation. |
NOT_CONFIGURED | 400 | WhatsApp is not configured for this channel. |
NO_RECIPIENTS | 400 | No valid recipients were found for a broadcast. |
WEBHOOK_VERIFICATION_FAILED | 400 | Your endpoint did not echo the verification challenge. |
NOT_FOUND | 404 | The requested resource does not exist. |
RATE_LIMIT_EXCEEDED | 429 | Too many requests — back off and retry. |
SESSION_EXPIRED | 400 | The 24-hour WhatsApp customer service window has expired. Send an approved template instead. |
INVALID_RECIPIENT | 400 | The phone number is not a WhatsApp user. |
TEMPLATE_ERROR | 400 | The named template was rejected by Meta (wrong language, wrong variable count, not approved, etc). |
UNSUPPORTED_TYPE | 400 | Meta does not support this message type for the recipient. |
MEDIA_FETCH_FAILED | 400 | We could not download the media URL you supplied; it must be a public HTTPS URL returning bytes. |
MEDIA_UPLOAD_FAILED | 502 | Meta rejected our upload of your media bytes. |
IDEMPOTENCY_CONFLICT | 409 | Same Idempotency-Key reused with a different request body. |
IDEMPOTENCY_IN_PROGRESS | 409 | A previous request with the same Idempotency-Key is still in flight. |
WHATSAPP_ERROR | 502 | Meta's WhatsApp API returned an error not covered above. |
INTERNAL_ERROR | 500 | An unexpected error occurred on our side. |
Account
Retrieve information about the workspace and channel tied to your API key.
/v1/accountcurl https://api.247rep.app/v1/account \
-H "Authorization: Bearer rep_live_your_key"Response
{
"workspace": { "name": "Acme Inc", "email": "owner@acme.com" },
"assistant": { "name": "Acme Support", "platform": "WHATSAPP" },
"whatsappNumber": "+2348012345678",
"creditsBalance": 4820,
"environment": "live",
"messagingTier": "TIER_1K",
"qualityRating": "GREEN"
}messagingTier and qualityRating are sourced from Meta and only present when the WhatsApp number is fully connected.Messages
Send WhatsApp messages and read message history.
/v1/messages/sendSend a WhatsApp message to a recipient.
Parameters
totypetextimage / video / documentaudiotemplatelocationSet the Idempotency-Key header on every send so a network retry never produces a duplicate message. See Idempotency for details.
curl -X POST https://api.247rep.app/v1/messages/send \
-H "Authorization: Bearer rep_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"to": "+2348012345678",
"type": "image",
"image": { "link": "https://example.com/photo.jpg", "caption": "Your order" }
}'Send a voice note or audio file
Use type: "audio" with a publicly reachable HTTPS link. WhatsApp supports MP3, OGG (Opus), AAC, AMR, and M4A. Audio costs 2 credits.
curl -X POST https://api.247rep.app/v1/messages/send \
-H "Authorization: Bearer rep_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"to": "+2348012345678",
"type": "audio",
"audio": { "link": "https://example.com/welcome.mp3" }
}'Response
{
"messageId": "cm5xyz...",
"whatsappMessageId": "wamid.HBgM...",
"status": "sent",
"creditsUsed": 4,
"creditsRemaining": 4816
}/v1/messagesList messages across all conversations, newest first.
Parameters
conversationIddirectionstatuslimitcursor{
"data": [
{
"id": "cm5...",
"conversationId": "cm4...",
"whatsappMessageId": "wamid...",
"direction": "outbound",
"senderType": "api",
"content": "Hello from 247Rep!",
"contentType": "text",
"mediaUrl": null,
"status": "delivered",
"createdAt": "2026-06-19T10:00:00.000Z",
"deliveredAt": "2026-06-19T10:00:02.000Z",
"readAt": null
}
],
"hasMore": false,
"nextCursor": null
}/v1/messages/{messageId}Retrieve a single message by ID, including media metadata and credits used.
Conversations
List and inspect conversations, fetch their messages, and close them.
/v1/conversationsList conversations, newest activity first.
Parameters
statuslimitcursor{
"data": [
{
"id": "cm4...",
"customerPhone": "+2348012345678",
"customerName": "Ada",
"customerEmail": null,
"customerAvatar": null,
"status": "active",
"messageCount": 12,
"lastMessagePreview": "Thanks!",
"lastMessageAt": "2026-06-19T09:58:00.000Z",
"firstMessageAt": "2026-06-10T08:00:00.000Z",
"createdAt": "2026-06-10T08:00:00.000Z",
"serviceWindowOpen": true
}
],
"hasMore": false,
"nextCursor": null
}serviceWindowOpen indicates whether the WhatsApp 24-hour customer service window is still open. When closed, you can only send approved template messages./v1/conversations/{conversationId}Retrieve a single conversation, including automation and escalation state.
/v1/conversations/{conversationId}/messagesList the messages within a conversation, newest first.
/v1/conversations/{conversationId}/closeMark a conversation as closed.
{ "success": true, "conversationId": "cm4...", "status": "closed" }Contacts
Contacts represent customers identified by phone number. Create or update them, list, retrieve, and delete.
/v1/contactsList contacts that have a phone number, newest activity first.
/v1/contactsCreate a contact, or update the existing one with the same phone number (upsert).
Parameters
phonenameemailtagsmetadatacurl -X POST https://api.247rep.app/v1/contacts \
-H "Authorization: Bearer rep_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "phone": "+2348012345678", "name": "Ada Lovelace", "email": "ada@example.com" }'Returns 201 Created for a new contact, or 200 OK when an existing contact was updated.
/v1/contacts/{contactId}Retrieve a single contact.
/v1/contacts/{contactId}Permanently delete a contact and its conversation history.
{ "success": true, "deleted": "cm4..." }Broadcasts
Send a message to many contacts at once. Broadcasts are queued and processed asynchronously.
/v1/broadcastQueue a broadcast to up to 1,000 contacts.
Parameters
contactIdsmessagecurl -X POST https://api.247rep.app/v1/broadcast \
-H "Authorization: Bearer rep_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"contactIds": ["cm4a...", "cm4b..."],
"message": { "type": "text", "text": { "body": "Flash sale today only!" } }
}'Response — 202 Accepted
{
"broadcastId": "cm6...",
"totalRecipients": 2,
"creditsUsed": 4,
"status": "sending"
}/v1/broadcast/{broadcastId}Check the delivery progress of a broadcast.
{
"id": "cm6...",
"message": "Flash sale today only!",
"totalRecipients": 2,
"sentCount": 2,
"failedCount": 0,
"status": "completed",
"createdAt": "2026-06-19T10:00:00.000Z",
"completedAt": "2026-06-19T10:00:30.000Z",
"delivered": 2,
"failed": 0,
"pending": 0
}Templates
List your approved WhatsApp message templates. Templates are required to message a customer outside the 24-hour service window.
/v1/templatesList approved message templates from Meta.
{
"data": [
{
"name": "order_confirmation",
"language": "en",
"category": "UTILITY",
"status": "APPROVED",
"components": [ /* ... */ ]
}
]
}APPROVED templates are returned. Results are cached briefly, so a newly approved template may take a short time to appear.Telegram — Overview
403 FORBIDDEN.Telegram is simpler than WhatsApp: there are no approved templates and no 24-hour service window. As long as a user has started a chat with your bot (or your bot is in the group), you can message them any time.
- The recipient is a Telegram
chat_id(a number) or an@username. - Send text, photos, videos, documents, or audio — each with an optional caption.
- No template approvals required, no message category fees.
- Delivery is real-time. We use Telegram's native Bot API on your behalf.
Telegram — Send message
One endpoint, five message types. Pick the type and supply the matching media URL.
/v1/telegram/sendSend a Telegram message to a chat.
Parameters
chatIdtypetexturlcaptionfilenameparseModedisableNotificationreplyToMessageIdText
curl -X POST https://api.247rep.app/v1/telegram/send \
-H "Authorization: Bearer rep_live_your_telegram_key" \
-H "Content-Type: application/json" \
-d '{
"chatId": 123456789,
"type": "text",
"text": "Your order has shipped."
}'Photo with caption
curl -X POST https://api.247rep.app/v1/telegram/send \
-H "Authorization: Bearer rep_live_your_telegram_key" \
-H "Content-Type: application/json" \
-d '{
"chatId": 123456789,
"type": "photo",
"url": "https://example.com/order.jpg",
"caption": "Your order"
}'Document
{
"chatId": "@mychannel",
"type": "document",
"url": "https://example.com/invoice.pdf",
"filename": "invoice.pdf",
"caption": "Your invoice"
}Audio (music or voice note)
Use type: "audio" with a publicly reachable HTTPS link. Telegram accepts MP3, M4A, and OGG. Audio costs 2 credits.
curl -X POST https://api.247rep.app/v1/telegram/send \
-H "Authorization: Bearer rep_live_your_telegram_key" \
-H "Content-Type: application/json" \
-d '{
"chatId": 123456789,
"type": "audio",
"url": "https://example.com/welcome.mp3",
"caption": "Welcome message"
}'Response
{
"messageId": "cm9...",
"telegramMessageId": 42,
"status": "sent",
"creditsUsed": 4,
"creditsRemaining": 4812
}Node example
const res = await fetch("https://api.247rep.app/v1/telegram/send", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.REP_KEY,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
chatId: 123456789,
type: "photo",
url: "https://example.com/order.jpg",
caption: "Your order",
}),
});
const data = await res.json();Python example
import os, uuid, requests
r = requests.post(
"https://api.247rep.app/v1/telegram/send",
headers={
"Authorization": f"Bearer {os.environ['REP_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"chatId": 123456789,
"type": "text",
"text": "Hello from Python!",
},
)
print(r.json())Telegram — Conversations
List Telegram conversations and read their messages.
/v1/telegram/conversationsList Telegram conversations, newest activity first.
Parameters
statuslimitcursor{
"data": [
{
"id": "cm8...",
"telegramChatId": "123456789",
"telegramUsername": "ada_lovelace",
"customerName": "Ada",
"status": "active",
"messageCount": 4,
"lastMessagePreview": "thanks!",
"lastMessageAt": "2026-06-19T09:58:00.000Z"
}
],
"hasMore": false,
"nextCursor": null
}/v1/telegram/conversations/{conversationId}/messagesList the messages within a Telegram conversation, newest first.
Email — Account
403 FORBIDDEN./v1/email/accountGet the connected email account and sync status.
{
"emailAddress": "support@acme.com",
"displayName": "Acme Support",
"provider": "gmail",
"syncStatus": "active",
"lastSyncAt": "2026-06-19T09:59:00.000Z",
"aiAutoReply": true
}Email — Send
Send an email from your connected account. Sends immediately (no undo window). The shape mirrors what Resend and similar services offer, so porting existing send code is straightforward.
/v1/email/sendSend an email message.
Parameters
tosubjecthtmltextfromreplyToccbccheadersattachmentsinReplyTothreadIdbodyHtml / bodyText — these are accepted as aliases for html / text for backward compatibility.HTML email
curl -X POST https://api.247rep.app/v1/email/send \
-H "Authorization: Bearer rep_live_your_email_key" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"to": "customer@example.com",
"subject": "Your order has shipped",
"html": "<p>Tracking: <strong>ABC123</strong></p>",
"text": "Tracking: ABC123"
}'Plain text only
{
"to": "customer@example.com",
"subject": "Quick note",
"text": "Hi, just confirming your appointment for tomorrow."
}With an attachment (base64)
{
"to": "customer@example.com",
"subject": "Your invoice",
"html": "<p>Invoice attached.</p>",
"attachments": [
{
"filename": "invoice.pdf",
"content": "JVBERi0xLjQKJ...",
"contentType": "application/pdf"
}
]
}With an attachment (URL)
{
"to": "customer@example.com",
"subject": "Your invoice",
"html": "<p>Invoice attached.</p>",
"attachments": [
{ "filename": "invoice.pdf", "url": "https://example.com/invoice.pdf" }
]
}Response
{
"messageId": "<api-1718...@247rep.app>",
"threadId": "cm7...",
"status": "sent",
"creditsUsed": 1,
"creditsRemaining": 4817
}Node example
import crypto from "node:crypto";
const res = await fetch("https://api.247rep.app/v1/email/send", {
method: "POST",
headers: {
Authorization: "Bearer " + process.env.REP_EMAIL_KEY,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
to: "customer@example.com",
subject: "Welcome!",
html: "<h1>Welcome</h1><p>Thanks for joining.</p>",
text: "Welcome! Thanks for joining.",
}),
});
const data = await res.json();Python example
import os, uuid, requests
r = requests.post(
"https://api.247rep.app/v1/email/send",
headers={
"Authorization": f"Bearer {os.environ['REP_EMAIL_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"to": "customer@example.com",
"subject": "Welcome!",
"html": "<h1>Welcome</h1><p>Thanks for joining.</p>",
},
)
print(r.json())Email — Threads
List email threads with folder and category filters.
/v1/email/threadsList email threads, newest activity first.
Parameters
foldercategoryisReadlimitcursor{
"data": [
{
"id": "cm7...",
"subject": "Refund request",
"contactEmail": "customer@example.com",
"contactName": "Ada",
"lastMessageAt": "2026-06-19T09:00:00.000Z",
"messageCount": 3,
"isRead": false,
"isStarred": false,
"category": "support",
"priority": "normal"
}
],
"hasMore": false,
"nextCursor": null
}/v1/email/threads/{threadId}Retrieve a single thread with its messages.
Email — Contacts
List unique email contacts, aggregated across threads.
/v1/email/contactsList deduplicated email contacts.
Parameters
searchlimitcursor{
"data": [
{
"email": "customer@example.com",
"name": "Ada",
"lastContactDate": "2026-06-19T09:00:00.000Z",
"messageCount": 5,
"linkedWhatsAppPhone": "+2348012345678"
}
],
"hasMore": false,
"nextCursor": null
}Managing webhooks
Register HTTPS endpoints to receive real-time events. WhatsApp and email each have their own webhook endpoints; use the key for the matching channel.
/v1/webhooksList registered WhatsApp webhooks.
/v1/webhooksRegister a new webhook (verified on creation).
Parameters
urleventssecretGET to your URL with a challenge query parameter. Your endpoint must respond with the exact challenge value as plain text, or registration fails with WEBHOOK_VERIFICATION_FAILED.curl -X POST https://api.247rep.app/v1/webhooks \
-H "Authorization: Bearer rep_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/webhooks/247rep",
"events": ["message.received", "message.delivered"],
"secret": "whsec_your_secret"
}'{
"id": "cm8...",
"url": "https://yourapp.com/webhooks/247rep",
"events": ["message.received", "message.delivered"],
"isActive": true,
"createdAt": "2026-06-19T10:00:00.000Z"
}/v1/webhooks/{webhookId}Delete a webhook.
Email webhooks
Email channels use the parallel /v1/email/webhooks and /v1/email/webhooks/{webhookId} endpoints with the same request/response shape.
Event types
Subscribe to the events relevant to your integration.
WhatsApp events
| Event | Fired when |
|---|---|
message.received | An inbound message arrives from a customer. |
message.sent | An outbound message is accepted by WhatsApp. |
message.delivered | A message is delivered to the recipient's device. |
message.read | A message is read by the recipient. |
message.failed | A message fails to send or deliver. |
Email events
| Event | Fired when |
|---|---|
email.received | An inbound email arrives. |
email.sent | An outbound email is sent. |
email.opened | A sent email is opened by the recipient. |
email.thread.categorized | The AI assigns a category to a thread. |
Payload
Events are delivered as a JSON POST with a consistent envelope:
{
"event": "message.received",
"timestamp": "2026-06-19T10:00:00.000Z",
"data": {
"conversationId": "cm4...",
"messageId": "wamid...",
"from": "+2348012345678",
"content": "Is this in stock?"
}
}Verifying signatures
When you register a webhook with a secret, every delivery is signed. Verify the X-247Rep-Signature header — an HMAC-SHA256 of the raw request body using your secret — to confirm the request genuinely came from 247Rep.
import crypto from "crypto";
function verifySignature(rawBody: string, signature: string, secret: string) {
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express example
app.post("/webhooks/247rep", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.header("X-247Rep-Signature") || "";
if (!verifySignature(req.body.toString(), sig, process.env.WEBHOOK_SECRET)) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body.toString());
// handle event...
res.status(200).send("ok");
});2xx status within a few seconds. Do heavy processing asynchronously so deliveries are not retried unnecessarily.