247Rep
REST API
A RESTful API to send messages, manage conversations and contacts, run broadcasts, send email, and receive real-time webhooks. Predictable resource-oriented URLs, JSON request and response bodies, and standard HTTP status codes.
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 | Media — a link you host | 1 credit (any type) |
| WhatsApp / Telegram | Media — uploaded to us (image/photo) | 2 credits |
| WhatsApp / Telegram | Media — uploaded to us (video) | 5 credits |
| WhatsApp / Telegram | Media — uploaded to us (document/audio) | 2 credits |
| Template message | 2 credits | |
| Any | Uploading a file to /v1/media | Free |
| Any email (with or without attachments) | 1 credit | |
| Broadcast | Per recipient (same as the underlying message type) |
/v1/media and the send is billed by type, because we store and re-serve it. See Messages → Two ways to attach media.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
{
"id": "clx9a8b7c6d5e4f3",
"assistantId": "cma1b2c3d4e5f6g7",
"keyId": "key_abc123",
"workspace": { "id": "clx9a8b7c6d5e4f3", "name": "Acme Inc", "email": "owner@acme.com" },
"assistant": { "id": "cma1b2c3d4e5f6g7", "name": "Acme Support", "platform": "WHATSAPP" },
"whatsappNumber": "+2348012345678",
"creditsBalance": 4820,
"environment": "live",
"messagingTier": "TIER_1K",
"qualityRating": "GREEN"
}id is the stable account/workspace identifier this key belongs to, and assistantId is the channel it drives — key your integration on these rather than the email, which can change. keyId tells you which key made the call.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.
Two ways to attach media
You can either point us at a file you host, or upload it to us — so you never have to expose your own storage publicly.
- You host it (flat 1 credit) — pass a public HTTPS
linkto your own file. We don't store it, so any media type is a flat 1 credit. - We host it (billed by type) — upload the bytes to
POST /v1/media, get back a hostedurl, then send that url. Because we store and re-serve it, the send is billed by type (image 2 · video 5 · doc/audio 2).
/v1/mediaUpload a file (multipart/form-data, field 'file', max 25MB) and get a hosted URL. Uploading is free — the send that uses the URL is billed by type.
# 1. Upload the file to us (no need to expose your own storage)
curl -X POST https://api.247rep.app/v1/media \
-H "Authorization: Bearer rep_live_your_key" \
-F "file=@invoice.pdf"
# → { "url": "https://...vercel-storage.com/247rep-api-media/.../invoice.pdf",
# "mimeType": "application/pdf", "size": 51234, "filename": "invoice.pdf" }
# 2. Send it — reference the hosted url (billed by type)
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": "document",
"document": { "link": "https://...vercel-storage.com/247rep-api-media/.../invoice.pdf", "filename": "invoice.pdf" }
}'The same /v1/media url works for Telegram too — pass it as url on /v1/telegram/send.
Parameters
totypetextimage / video / documentaudiotemplatelocationreplyToSet 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.
Reply to a message
Add replyTo to any send to thread it as a WhatsApp quoted reply under an earlier message. Use the message id you received from the message.received webhook or GET /v1/messages (the raw wamid works too). It only applies to conversational types — template sends can't be quoted.
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": "Thanks — that works for me!" },
"replyTo": "cm5_the_message_you_are_replying_to"
}'/v1/messages/{messageId}/reactReact to a message with an emoji — the same affordance as the dashboard.
messageId is the 247Rep message id (from a webhook or GET /v1/messages). Send an empty emoji to remove a reaction you previously sent.
# Add a reaction
curl -X POST https://api.247rep.app/v1/messages/cm5.../react \
-H "Authorization: Bearer rep_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "emoji": "👍" }'
# Remove it
curl -X POST https://api.247rep.app/v1/messages/cm5.../react \
-H "Authorization: Bearer rep_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "emoji": "" }'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",
"referral": null,
"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.Ad attribution (Click-to-WhatsApp)
When a customer reaches the business by tapping a Click-to-WhatsApp ad (or a post call-to-action), the conversation carries a referral object describing the ad they came from. It is null for conversations that did not originate from an ad, and is present on both GET /v1/conversations and GET /v1/conversations/{conversationId}.
"referral": {
"source_type": "ad",
"source_id": "120209...", // Meta ad id — the ad this lead came from
"source_url": "https://fb.me/...",
"headline": "50% off this weekend",
"body": "Message us to claim your code",
"media_type": "image",
"image_url": "https://...",
"ctwa_clid": "ARB2...", // click id for Meta's Conversions API
"captured_at": "2026-06-10T08:00:00.000Z"
}Parameters
source_idsource_typesource_urlheadlinebodymedia_typeimage_url / video_url / thumbnail_urlctwa_clidcaptured_atsource_id, source_url, or ctwa_clid is guaranteed when a referral is present./v1/conversations/{conversationId}Retrieve a single conversation, including automation and escalation state.
/v1/conversations/{conversationId}Update a conversation. Set automationEnabled to pause or resume AI auto-reply on this thread.
Parameters
automationEnabled# Pause the AI on this conversation (a human takes over)
curl -X PATCH https://api.247rep.app/v1/conversations/cm4.../ \
-H "Authorization: Bearer rep_live_your_key" \
-H "Content-Type: application/json" \
-d '{ "automationEnabled": false }'
# → { "success": true, "id": "cm4...", "automationEnabled": false, "status": "active" }/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 numeric Telegram
chatId(e.g.123456789) — not a phone number and not a user’s @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.
chatId. Telegram bots can only message a user who has started the bot first — you can’t cold-message anyone. You get their chatId from an inbound message: the message.received webhook, or GET /v1/telegram/conversations (the telegramChatId field). Reply using that id. An @channelusername works only for public channels your bot administers, never for individual users.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
chatIdtypetexturlcaptionfilenameparseModereplyToText
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())Reply to a message
Add replyTo to thread a send under an earlier message. Because Telegram message ids are only unique within a chat, we resolve it against the chatId you send — pass the 247Rep message id (from the message.received webhook or the conversation messages endpoint) or Telegram's numeric message_id.
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": "On it — shipping today!",
"replyTo": "cm9_or_the_numeric_message_id"
}'/v1/telegram/messages/{messageId}/reactReact to a Telegram message with an emoji (setMessageReaction).
messageId is the 247Rep message id. Send an empty emoji to remove your reaction. Telegram only accepts a fixed set of reaction emoji (👍 👎 ❤️ 🔥 🥰 👏 😁 🤔 🎉 …); an unsupported emoji returns a 400.
curl -X POST https://api.247rep.app/v1/telegram/messages/cm9.../react \
-H "Authorization: Bearer rep_live_your_telegram_key" \
-H "Content-Type: application/json" \
-d '{ "emoji": "🔥" }'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.Data Connections — let the AI call your API
The REST API above is your app calling 247Rep. Data Connections are the reverse: 247Rep calling your app. You register your own API once, describe its endpoints in plain language, and the AI calls them live inside a conversation — so “where's my order?” hits your tracking system and the assistant answers with the real, current status.
Six auth types are supported, secrets are encrypted at rest (AES-256-GCM), requests are SSRF-guarded and rate-limited per connection, and every call is written to a log you can inspect.
Setup steps
The New Connection wizard walks through four steps, then you add one or more endpoints.
- Details — give the connection a name and description, set the base URL of your API (e.g.
https://api.yourstore.com), and optionally tune the request timeout and per-minute rate limit. - Auth — choose how we authenticate to your API (see Auth types) and paste the credential. It's encrypted before it ever touches the database.
- Test — we make a live call to confirm the base URL and credentials work before anything is saved.
- Add endpoints — for each action the AI should be able to take, define:
- Method + path — e.g.
GET /orders/{orderId}. - Parameters — path, query, and request-body fields the AI should fill in.
- Response mapping — which fields of your response the AI should read back (so it quotes the tracking number, not the raw JSON).
- Context instructions — plain-English guidance telling the AI when and how to use this endpoint (“call this when a customer asks about an order status; ask for their order number first”).
- Contact identifier — which field identifies the customer (e.g. their phone), so the AI looks up the right person automatically.
- Method + path — e.g.
Auth types
Pick whichever matches how your API authenticates. The credential is encrypted at rest.
Supported auth
nonebearerapi_key_headerapi_key_querybasic_authoauth2_client_credentialscustom_header