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

Live

BadCompadres 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)

Live

During 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

Live
Organization → Project → Environment → Customer → Connection → WABA → Phone number
ConceptMeaning in your SaaS
OrganizationYour company (the developer account).
ProjectOne of your SaaS products.
Environmentsandbox or production. Keys, customers and connections never cross.
CustomerOne of YOUR tenants (a repair shop, a store). Identified by your external_customer_id.
ConnectionThe 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 24h

Authentication

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/json

Base 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..."
  }
}
HTTPMeaning
400Validation failed
401Missing / invalid / revoked key
403Key not allowed for this environment or production not enabled
404Customer, connection or template not found in this environment
409Duplicate external_customer_id or idempotency conflict
422Meta rejected the operation (see meta_code)
429Rate 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 approval

Connect 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)
StatusMeaning
not_configuredCustomer exists, onboarding not started
pendingOnboarding in progress
connectedReady to send
action_requiredTenant must fix something in Meta (payment method, display name)
restrictedMeta limited the number (quality / policy)
disconnectedRevoked by tenant or by you
errorOnboarding failed; create a new session

Templates

Live

Templates 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)
SituationWhat you can send
You message first (notification, reminder, campaign)type: template only
Customer wrote to the tenant in the last 24htype: text, image, document, interactive — or template
Customer replies againThe 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.

EventWhen
connection.connectedTenant finished onboarding
connection.updatedStatus changed (action_required, restricted, disconnected)
message.receivedEnd customer wrote to the tenant (opens 24h window)
message.statussent | delivered | read | failed (with meta_code)
template.status_updatedMeta 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 eventMessageTemplate (UTILITY unless noted)
Device checked inTemplate + portal buttondevice_received: “Hola {{1}}, recibimos tu {{2}}, orden #{{3}}.” [Ver estatus]
Status changedTemplateorder_status: “Tu orden #{{1}} ahora está: {{2}}.”
Part / service addedTemplate + authorize buttonapproval_request: “Agregamos {{1}} por ${{2}} a tu orden #{{3}}.” [Autorizar]
Customer asks a questionWebhook message.receivedShow in your inbox; reply with type text within 24h
Work finishedTemplatedevice_ready: “Tu {{1}} está listo para recoger. Total ${{2}}.”
Pickup reminder (3 days)Templatepickup_reminder
6-month maintenance offerTemplate (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.
RequirementDetail
WhatsApp Business AppThe number must be on the Business App (not personal WhatsApp), recently active, on a supported app version
Phone accessThe owner must have the phone in hand to scan the QR / confirm in the app during Embedded Signup
Country supportAvailable in most markets; some regions are excluded by Meta — check Meta's current list
Meta app accessCoexistence 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.
TopicBehavior with coexistence
24-hour windowShared: a customer message opens it for both the phone and the API
TemplatesSent only through the API; visible on the phone afterwards
Replies from the phoneDelivered to your SaaS as echo events so your inbox stays complete
ThroughputLower than a dedicated API number (Meta limits messages per second)
Not supported on the phone after linkingSome app features (e.g. broadcast lists, disappearing messages) are disabled by Meta
DisconnectTenant 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
CapabilityStatus
Portal: organizations, projects, environments, customersLive
Embedded Signup + encrypted credentials (portal)Live — awaiting Meta App Review
Inbound Meta webhooksLive
Send messages & manage templates (portal)Live
API keysNext
REST API v1 (customers, sessions, messages, templates)Next
Signed developer webhooks with retriesNext
Hosted onboarding page (/connect/onboard)Next
Usage metering, self-service signup, billingLater

Endpoints marked “Planned (v1 contract)” document the stable contract being implemented; field names will not change when they go live.