API Documentation
Add official WhatsApp to every tenant of your SaaS. Status labels: Live · Planned (v1 contract) · Requires Meta approval
Base URL: https://badcompadres.com/api/public/v1
Introduction
LiveBadCompadres Connect is API-first infrastructure that lets your SaaS offer the official WhatsApp Business Cloud API to each of its own tenants. You build the product experience (work orders, portals, inboxes); Connect handles Meta onboarding, encrypted credentials, templates, message delivery and webhooks.
Your tenants never touch Meta Developers. You never store Meta tokens. You call one REST API with one key per environment.
Access model (managed beta)
LiveDuring the managed beta, developer accounts and projects are provisioned by a BadCompadres superadmin on request. There is no public self-signup or billing yet; the data model already supports self-service, which will be enabled later without breaking changes.
- ▸1. Request access at gerardo@badcompadres.com. A superadmin creates your organization and invites you.
- ▸2. A superadmin creates one project per SaaS product you want to connect (e.g. “Mobile Retail Hub”).
- ▸3. Sign in at /connect/login, open the project, and create an API key for the sandbox environment.
- ▸4. Register your webhook URL, integrate the endpoints below, then request production enablement.
Core concepts
LiveOrganization → Project → Environment → Customer → Connection → WABA → Phone number| Concept | Meaning in your SaaS |
|---|---|
| Organization | Your company (the developer account). |
| Project | One of your SaaS products. |
| Environment | sandbox or production. Keys, customers and connections never cross. |
| Customer | One of YOUR tenants (a repair shop, a store). Identified by your external_customer_id. |
| Connection | The tenant’s linked WhatsApp Business Account + phone number. |
Isolation is enforced in the database (row level security + composite foreign keys), not only in the UI.
Quickstart
Planned (v1 contract)Five calls take a tenant from zero to sending WhatsApp messages:
# 1. Register your tenant
curl -X POST https://badcompadres.com/api/public/v1/customers \
-H "Authorization: Bearer $BC_API_KEY" -H "Content-Type: application/json" \
-d '{"external_customer_id":"shop_42","name":"Taller San Pedro"}'
# 2. Create an onboarding link and send your tenant there
curl -X POST https://badcompadres.com/api/public/v1/connect/sessions \
-H "Authorization: Bearer $BC_API_KEY" -H "Content-Type: application/json" \
-d '{"external_customer_id":"shop_42","return_url":"https://yoursaas.com/settings/whatsapp"}'
# 3. Wait for the webhook connection.connected
# 4. Send a template (business-initiated)
curl -X POST https://badcompadres.com/api/public/v1/messages \
-H "Authorization: Bearer $BC_API_KEY" -H "Idempotency-Key: order-981-received" \
-H "Content-Type: application/json" \
-d '{"external_customer_id":"shop_42","to":"+526621234567","type":"template",
"template":{"name":"device_received","language":"es_MX","variables":["Carlos","iPhone 13","981"]}}'
# 5. Receive replies via webhook message.received and answer with type "text" within 24hAuthentication
Planned (v1 contract)Every request carries an environment-scoped secret key as a bearer token. Keys start with bc_test_ (sandbox) or bc_live_ (production), are shown once, and are stored hashed. Call the API only from your server — never ship a key to a browser or mobile app.
Authorization: Bearer bc_test_9f2c...
Content-Type: application/jsonBase URL: https://badcompadres.com/api/public/v1. All bodies are JSON. Timestamps are ISO-8601 UTC. Phone numbers are E.164 (+526621234567).
Errors, idempotency & limits
Planned (v1 contract){
"error": {
"type": "invalid_request", // invalid_request | authentication | permission | not_found | conflict | rate_limited | meta_error | internal
"code": "outside_24h_window",
"message": "Free-form text requires a customer reply in the last 24 hours. Use a template.",
"meta_code": 131047, // present when Meta rejected the call
"request_id": "req_01HF..."
}
}| HTTP | Meaning |
|---|---|
| 400 | Validation failed |
| 401 | Missing / invalid / revoked key |
| 403 | Key not allowed for this environment or production not enabled |
| 404 | Customer, connection or template not found in this environment |
| 409 | Duplicate external_customer_id or idempotency conflict |
| 422 | Meta rejected the operation (see meta_code) |
| 429 | Rate limited — retry after Retry-After seconds |
Send an Idempotency-Key header on POST /messages (e.g. your order ID + event). Repeating the same key within 24h returns the original result instead of sending twice.
Customers (your tenants)
Planned (v1 contract)POST /customers create { external_customer_id, name, email? }
GET /customers?limit=50&cursor=... list
GET /customers/{external_customer_id} retrieve (includes connection summary)
PATCH /customers/{external_customer_id} update { name?, email? }{
"id": "cus_7d1e...",
"external_customer_id": "shop_42",
"name": "Taller San Pedro",
"environment": "sandbox",
"connection": { "status": "connected", "phone_number": "+52 662 411 5231", "display_name": "Taller San Pedro" },
"created_at": "2026-10-24T01:00:00Z"
}external_customer_id is unique per project + environment. Use your own tenant ID so you never need to store ours.
Tenant onboarding (Meta Embedded Signup)
Requires Meta approvalConnect runs the official Meta Embedded Signup for you (hosted onboarding, like Stripe Connect). Your app requests a short-lived link; your tenant opens it, logs in with Facebook, picks or registers their WhatsApp number, and is redirected back to you.
POST /connect/sessions
{ "external_customer_id": "shop_42", "return_url": "https://yoursaas.com/settings/whatsapp" }
→ 201
{ "url": "https://badcompadres.com/connect/onboard/ses_4b9...", "expires_at": "2026-10-24T02:00:00Z" }- ▸Open url in a new tab or popup (Meta’s dialog is a popup; avoid iframes).
- ▸On finish the tenant returns to return_url?status=connected|cancelled|error&customer=shop_42.
- ▸Treat the redirect as UX only — the source of truth is the connection.connected webhook.
- ▸Sessions expire after 60 minutes and are single-use.
// Next/TanStack server function in YOUR SaaS
export async function startWhatsAppOnboarding(tenantId: string) {
const res = await fetch("https://badcompadres.com/api/public/v1/connect/sessions", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.BC_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ external_customer_id: tenantId, return_url: "https://yoursaas.com/settings/whatsapp" }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
return (await res.json()).url as string; // redirect the tenant here
}Connections
Planned (v1 contract)GET /customers/{external_customer_id}/connection
DELETE /customers/{external_customer_id}/connection disconnect (stops sending, keeps history)| Status | Meaning |
|---|---|
| not_configured | Customer exists, onboarding not started |
| pending | Onboarding in progress |
| connected | Ready to send |
| action_required | Tenant must fix something in Meta (payment method, display name) |
| restricted | Meta limited the number (quality / policy) |
| disconnected | Revoked by tenant or by you |
| error | Onboarding failed; create a new session |
Templates
LiveTemplates are required for any business-initiated message (outside the 24h window). They belong to each tenant’s WABA, so your SaaS usually creates the same set for every new tenant right after it connects. Meta reviews them (UTILITY: usually minutes; MARKETING: longer).
GET /api/public/v1/templates?connection_id={uuid}
POST /api/public/v1/templates{
"connection_id": "6f1c…-uuid",
"name": "device_ready",
"language": "es_MX",
"category": "UTILITY",
"body": "Hola {{1}}, tu {{2}} (orden #{{3}}) está listo para recoger.",
"examples": ["Carlos", "iPhone 13", "981"]
}
// 201 → { "data": { "id": "…", "name": "device_ready", "status": "PENDING" } }- ▸Names: lowercase, digits and underscores. Same name + different language = translations of one template.
- ▸Variables must be sequential {{1}}, {{2}}… and cannot start or end the body.
- ▸Use a URL button with a variable suffix for portal / authorization links — they get higher click rates and avoid link-in-body rejections.
- ▸Status updates arrive via the template.status_updated webhook (APPROVED, REJECTED, PAUSED).
Messages & the 24-hour window
Planned (v1 contract)| Situation | What you can send |
|---|---|
| You message first (notification, reminder, campaign) | type: template only |
| Customer wrote to the tenant in the last 24h | type: text, image, document, interactive — or template |
| Customer replies again | The 24h window restarts from that reply |
POST /messages
Idempotency-Key: order-981-ready
// template
{ "external_customer_id": "shop_42", "to": "+526621234567", "type": "template",
"template": { "name": "device_ready", "language": "es_MX",
"variables": ["Carlos", "iPhone 13", "981"], "button_variables": ["981"] } }
// free-form reply (inside 24h)
{ "external_customer_id": "shop_42", "to": "+526621234567", "type": "text",
"text": "El total es $1,250 MXN. Abrimos de 9 a 6.", "reply_to": "wamid.HBgM..." }
→ 202
{ "id": "msg_01HF...", "wamid": "wamid.HBgM...", "status": "accepted" }GET /customers/{external_customer_id}/conversations/{phone}
→ { "window_open": true, "window_expires_at": "2026-10-25T01:12:00Z", "last_inbound_at": "..." }Check window_open before offering a free-text reply box in your UI; if closed, show your template picker instead. Delivery progress (sent → delivered → read, or failed) arrives via webhooks.
Webhooks to your SaaS
Planned (v1 contract)Register one HTTPS endpoint per environment in the portal (Webhooks). Connect POSTs JSON events, signed with your endpoint secret, and retries with exponential backoff for 24h on non-2xx responses. Respond 200 fast and process asynchronously; dedupe on event.id.
| Event | When |
|---|---|
| connection.connected | Tenant finished onboarding |
| connection.updated | Status changed (action_required, restricted, disconnected) |
| message.received | End customer wrote to the tenant (opens 24h window) |
| message.status | sent | delivered | read | failed (with meta_code) |
| template.status_updated | Meta approved / rejected / paused a template |
POST https://yoursaas.com/api/webhooks/badcompadres
X-BC-Signature: t=1761268320,v1=5f8a... // HMAC-SHA256(secret, `${t}.${rawBody}`)
{
"id": "evt_01HF...",
"type": "message.received",
"environment": "production",
"created_at": "2026-10-24T01:12:00Z",
"data": {
"external_customer_id": "shop_42",
"from": "+526621234567",
"profile_name": "Carlos",
"message": { "wamid": "wamid.HBgM...", "type": "text", "text": "¿Ya quedó mi equipo?" },
"window_expires_at": "2026-10-25T01:12:00Z"
}
}// Verify in your SaaS (Web Crypto — works on Node, Workers, Deno)
async function verify(raw: string, header: string, secret: string) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; // replay guard
const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(secret),
{ name: "HMAC", hash: "SHA-256" }, false, ["sign"]);
const sig = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(`${parts.t}.${raw}`));
const hex = [...new Uint8Array(sig)].map((b) => b.toString(16).padStart(2, "0")).join("");
return hex === parts.v1; // use a constant-time compare in production
}Integrating from a Lovable (or any) SaaS
Planned (v1 contract)- ▸1. Store the key as a server secret named BC_API_KEY (never a VITE_ / public variable).
- ▸2. Create server functions: startWhatsAppOnboarding, sendWhatsApp, listTemplates. Call Connect only from there.
- ▸3. Add a public route /api/public/badcompadres/webhook that verifies X-BC-Signature, then updates your orders / inbox.
- ▸4. Map your tenant ID to external_customer_id — no extra table needed; optionally cache connection status.
- ▸5. On connection.connected, provision your default templates for that tenant.
- ▸6. Add a “Connect WhatsApp” button in tenant settings that redirects to the onboarding URL.
Prompt you can paste into Lovable in your SaaS project: “Integrate BadCompadres Connect following https://badcompadres.com/connect/docs. Secret BC_API_KEY. Add tenant onboarding button, server functions for templates and messages, and a signed webhook route that stores inbound messages and delivery statuses.”
Use case: repair shop / service SaaS
Planned (v1 contract)| SaaS event | Message | Template (UTILITY unless noted) |
|---|---|---|
| Device checked in | Template + portal button | device_received: “Hola {{1}}, recibimos tu {{2}}, orden #{{3}}.” [Ver estatus] |
| Status changed | Template | order_status: “Tu orden #{{1}} ahora está: {{2}}.” |
| Part / service added | Template + authorize button | approval_request: “Agregamos {{1}} por ${{2}} a tu orden #{{3}}.” [Autorizar] |
| Customer asks a question | Webhook message.received | Show in your inbox; reply with type text within 24h |
| Work finished | Template | device_ready: “Tu {{1}} está listo para recoger. Total ${{2}}.” |
| Pickup reminder (3 days) | Template | pickup_reminder |
| 6-month maintenance offer | Template (MARKETING) | Requires opt-in; only to customers who accepted promotions |
Authorization links: generate a signed, single-use URL in your SaaS (e.g. /o/981/approve?t=...), pass its suffix as the URL button variable, and record the approval in your system. Connect never handles the approval itself.
Use case: marketing & retail notifications
Planned (v1 contract)- ▸Collect explicit WhatsApp opt-in per end customer and store it in your SaaS; Meta can restrict numbers that spam.
- ▸Use MARKETING templates for promotions, UTILITY for transactional updates — mixing them causes rejections.
- ▸Throttle bulk sends (your loop + Idempotency-Key per recipient); respect 429 Retry-After.
- ▸Honor “STOP / BAJA” replies received via message.received by flagging the contact as opted-out.
- ▸Meta charges per conversation category; usage reporting per tenant is on the roadmap.
Coexistence: keep the WhatsApp Business App on the same number
Requires Meta approval- ▸Many businesses already use a long-standing number in the WhatsApp Business App. With Meta Coexistence, that same number can be onboarded to the Cloud API through Connect while staff keep using the app on their phone.
- ▸Messages sent by your SaaS (templates, notifications) appear in the phone's chat history; replies typed on the phone are mirrored as echo events, and inbound customer messages reach both the phone and your webhook.
| Requirement | Detail |
|---|---|
| WhatsApp Business App | The number must be on the Business App (not personal WhatsApp), recently active, on a supported app version |
| Phone access | The owner must have the phone in hand to scan the QR / confirm in the app during Embedded Signup |
| Country support | Available in most markets; some regions are excluded by Meta — check Meta's current list |
| Meta app access | Coexistence onboarding must be enabled for the Connect Meta app (pending App Review) |
- ▸1. Your SaaS starts onboarding exactly as usual (Connect WhatsApp button in tenant settings).
- ▸2. In Meta's popup the tenant chooses “Connect your existing WhatsApp Business App”.
- ▸3. The tenant confirms on the phone; Meta can optionally import up to 6 months of chat history.
- ▸4. The connection becomes connected; the phone keeps working — no number migration or downtime.
| Topic | Behavior with coexistence |
|---|---|
| 24-hour window | Shared: a customer message opens it for both the phone and the API |
| Templates | Sent only through the API; visible on the phone afterwards |
| Replies from the phone | Delivered to your SaaS as echo events so your inbox stays complete |
| Throughput | Lower than a dedicated API number (Meta limits messages per second) |
| Not supported on the phone after linking | Some app features (e.g. broadcast lists, disappearing messages) are disabled by Meta |
| Disconnect | Tenant can unlink from the app; Connect marks the connection disconnected |
- ▸Recommended for: repair shops, stores and clinics that chat with customers at the counter while the SaaS automates order status, authorizations and pickup notices.
- ▸Prefer a dedicated new number for high-volume marketing or purely transactional (OTP/alerts) use.
- ▸Design your inbox so a human reply from the phone and an automated reply from the SaaS do not collide (e.g. pause bots after an echo event).
Security
Live- ▸Meta access tokens are encrypted with AES-GCM and only decrypted server-side; no client can read them.
- ▸Inbound Meta webhooks are verified with X-Hub-Signature-256 before storage and deduplicated.
- ▸Row level security on every table; owner / admin / developer / viewer roles; append-only audit log.
- ▸API keys stored as SHA-256 hashes, scoped to one environment, revocable instantly.
Availability & roadmap
Live| Capability | Status |
|---|---|
| Portal: organizations, projects, environments, customers | Live |
| Embedded Signup + encrypted credentials (portal) | Live — awaiting Meta App Review |
| Inbound Meta webhooks | Live |
| Send messages & manage templates (portal) | Live |
| API keys | Next |
| REST API v1 (customers, sessions, messages, templates) | Next |
| Signed developer webhooks with retries | Next |
| Hosted onboarding page (/connect/onboard) | Next |
| Usage metering, self-service signup, billing | Later |
Endpoints marked “Planned (v1 contract)” document the stable contract being implemented; field names will not change when they go live.