Embed Ranvedix in your own product. Manage tenants and WhatsApp connections programmatically, generate co-branded onboarding links, and receive signed lifecycle webhooks — without becoming a BSP.
All Platform API requests go to the /v1 base and authenticate with a Platform API key (created under Settings → Platform). Send it as a bearer token. Keys are scoped to your organization and are shown only once at creation.
Base URL
https://api.ranvedix.com/v1
Authorization header
Authorization: Bearer rvdx_live_xxxxxxxxxxxxxxxxxxxxOpenAPI: a machine-readable spec for these endpoints is at https://api.ranvedix.com/v1/openapi.json — import it into Postman or generate a client SDK.
Requests without a valid key return 401 Unauthorized. Treat keys like passwords — rotate them from the dashboard if exposed.
Scopes: keys carry scopes — connections:read, tenants:read, tenants:write (a:write scope implies the matching :read). A request to an endpoint your key isn’t scoped for returns 403 Forbidden. Issue a read-only key for dashboards and reporting; a write key for provisioning.
Rate limit: 120 requests/minute per API key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (seconds until the window resets) so you can self-throttle. Exceeding it returns 429 Too Many Requests with a Retry-After header. Need more? Ask us.
Errors
Every error returns a consistent JSON envelope and the matching HTTP status:
{ "error": { "type": "forbidden", "message": "this API key lacks the 'tenants:write' scope" } }type is a stable machine-readable slug (invalid_request, unauthorized, forbidden, not_found, rate_limited); message is human-readable.
Ranvedix onboards your number and routes messages — you send and receive directly through Meta’s official Cloud API, so your traffic never passes through us (and your number is never at ban risk from an unofficial bridge). Grab the number’s phone-number ID and a sending token from Connections → your number → Using this connection → Reveal sending token.
Send — cURL
curl -X POST https://graph.facebook.com/v23.0/<PHONE_NUMBER_ID>/messages \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"messaging_product":"whatsapp","to":"<RECIPIENT>","type":"text","text":{"body":"Hello from Ranvedix!"}}'Send — Node.js
await fetch(`https://graph.facebook.com/v23.0/${PHONE_NUMBER_ID}/messages`, {
method: "POST",
headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: JSON.stringify({
messaging_product: "whatsapp",
to: recipient,
type: "text",
text: { body: "Hello from Ranvedix!" },
}),
});Send — Python
import httpx
httpx.post(
f"https://graph.facebook.com/v23.0/{PHONE_NUMBER_ID}/messages",
headers={"Authorization": f"Bearer {TOKEN}"},
json={"messaging_product": "whatsapp", "to": recipient,
"type": "text", "text": {"body": "Hello from Ranvedix!"}},
)Receiving: set your Webhook Override URL on the connection — Meta delivers every inbound message there in its standard webhook format. Verify the signature exactly as in the Lifecycle webhookssection below, then handle and store it on your side (we’re message-blind — your conversation history lives with you).
Point your connection’s Webhook Override URL at an endpoint like this. Meta verifies it once (the hub.challenge handshake), then POSTs every inbound message signed with X-Hub-Signature-256 (HMAC of the raw body with your Meta app secret). Copy-paste, set two env vars, deploy.
Node.js / Express
import express from "express";
import crypto from "node:crypto";
const app = express();
const { VERIFY_TOKEN, APP_SECRET } = process.env;
// 1) One-time verification handshake.
app.get("/webhook/whatsapp", (req, res) => {
if (req.query["hub.mode"] === "subscribe" && req.query["hub.verify_token"] === VERIFY_TOKEN) {
return res.send(req.query["hub.challenge"]);
}
res.sendStatus(403);
});
// 2) Inbound messages — verify the signature, then handle.
app.post("/webhook/whatsapp", express.raw({ type: "*/*" }), (req, res) => {
const expected = "sha256=" + crypto.createHmac("sha256", APP_SECRET).update(req.body).digest("hex");
if (req.headers["x-hub-signature-256"] !== expected) return res.sendStatus(401);
const body = JSON.parse(req.body);
for (const entry of body.entry ?? []) {
for (const change of entry.changes ?? []) {
for (const msg of change.value?.messages ?? []) {
console.log("from", msg.from, "·", msg.type, "·", msg.text?.body ?? "");
// → store it, reply via the Cloud API, run your logic. It's yours.
}
}
}
res.sendStatus(200); // ack fast so Meta doesn't retry
});
app.listen(3000);Python / FastAPI
import hashlib, hmac, os
from fastapi import FastAPI, Request, Response
app = FastAPI()
VERIFY_TOKEN, APP_SECRET = os.environ["VERIFY_TOKEN"], os.environ["APP_SECRET"]
@app.get("/webhook/whatsapp")
async def verify(request: Request):
q = request.query_params
if q.get("hub.mode") == "subscribe" and q.get("hub.verify_token") == VERIFY_TOKEN:
return Response(q.get("hub.challenge", ""))
return Response(status_code=403)
@app.post("/webhook/whatsapp")
async def receive(request: Request):
raw = await request.body()
expected = "sha256=" + hmac.new(APP_SECRET.encode(), raw, hashlib.sha256).hexdigest()
if request.headers.get("x-hub-signature-256") != expected:
return Response(status_code=401)
body = await request.json()
for entry in body.get("entry", []):
for change in entry.get("changes", []):
for msg in change.get("value", {}).get("messages", []):
print("from", msg["from"], "·", msg["type"])
# → store it, reply via the Cloud API, run your logic. It's yours.
return {"ok": True}Set this URL + verify token on the connection (or in the onboarding dialog). To reply, use the Quickstartsend call above with the same number’s token.
Prefer no-code? Point your Webhook Override at an n8n Webhook node and build the rest visually — Ranvedix stays out of the message path either way.
hub.challenge query param on GET.// n8n Code node — verify Meta's signature on the raw body
const crypto = require("crypto");
const sig = $input.item.headers["x-hub-signature-256"];
const raw = JSON.stringify($input.item.body);
const expected = "sha256=" + crypto.createHmac("sha256", $env.APP_SECRET).update(raw).digest("hex");
if (sig !== expected) throw new Error("bad signature");
return $input.item;Then wire the message into any of n8n’s 400+ integrations (CRM, sheets, AI, your DB). To send replies, call the Cloud API (HTTP Request node) with the number’s token — see the Quickstart above.Note: n8n re-serializes the body, so if signatures mismatch, capture the raw body upstream instead.
List endpoints are paginated with limit (1–100, default 25) and offset, and return a pagination object with has_more.
/v1/healthVerify your API key and that the service is reachable — the first call to make when wiring up an integration or a status-page probe. Returns the org your key is scoped to.
curl https://api.ranvedix.com/v1/health \
-H "Authorization: Bearer $RANVEDIX_KEY"Response
{
"status": "ok",
"org_id": "org_4f…"
}/v1/connectionsEvery WhatsApp connection in your organization, newest first. Operational metadata only — never message content. Paginated.
curl "https://api.ranvedix.com/v1/connections?limit=25&offset=0" \
-H "Authorization: Bearer $RANVEDIX_KEY"Response
{
"connections": [
{
"id": "c3a1…",
"tenant_id": "t_92…",
"state": "live",
"coexistence": false,
"display_phone_number": "+1 555 0142",
"phone_number_id": "1099…",
"waba_id": "2728…",
"quality_rating": "GREEN",
"messaging_tier": "TIER_1K",
"heartbeat_status": "healthy",
"created_at": "2026-06-01T12:00:00Z"
}
],
"pagination": { "limit": 25, "offset": 0, "has_more": false }
}/v1/connections/{id}Fetch a single connection by id. Returns 404 if it does not belong to your organization.
curl https://api.ranvedix.com/v1/connections/c3a1… \
-H "Authorization: Bearer $RANVEDIX_KEY"Response
{
"id": "c3a1…",
"state": "live",
"quality_rating": "GREEN",
"messaging_tier": "TIER_1K",
"display_phone_number": "+1 555 0142"
}/v1/tenantsYour end-customers. Each connection belongs to a tenant, letting you segment connections by the customer you onboarded.
curl https://api.ranvedix.com/v1/tenants \
-H "Authorization: Bearer $RANVEDIX_KEY"Response
{
"tenants": [
{ "id": "t_92…", "external_ref": "acct_771", "name": "Bella Salon", "created_at": "2026-06-01T12:00:00Z" }
]
}/v1/tenantsRegister an end-customer. Pass your own external_ref to map it back to a record in your system — idempotent on external_ref, so a retry with the same value returns the existing tenant instead of duplicating.
curl -X POST https://api.ranvedix.com/v1/tenants \
-H "Authorization: Bearer $RANVEDIX_KEY" \
-H "Content-Type: application/json" \
-d '{ "name": "Bella Salon", "external_ref": "acct_771" }'Response
{ "id": "t_92…", "external_ref": "acct_771", "name": "Bella Salon", "created_at": "2026-06-13T12:00:00Z" }/v1/tenants/{id}/connectionsAll WhatsApp connections belonging to a specific tenant. Useful for displaying a customer's connected numbers within your own product. Paginated.
curl https://api.ranvedix.com/v1/tenants/t_92…/connections \
-H "Authorization: Bearer $RANVEDIX_KEY"Response
{
"connections": [
{
"id": "c3a1…",
"tenant_id": "t_92…",
"state": "live",
"display_phone_number": "+1 555 0142",
"quality_rating": "GREEN",
"created_at": "2026-06-01T12:00:00Z"
}
],
"pagination": { "limit": 25, "offset": 0, "has_more": false }
}/v1/eventsThe same events we deliver to your webhook, queryable after the fact — reconcile or catch up on anything missed during an endpoint outage. Filter by type, paginated, newest first.
curl "https://api.ranvedix.com/v1/events?type=connection.live&limit=25" \
-H "Authorization: Bearer $RANVEDIX_KEY"Response
{
"events": [
{
"id": "evt_01j…",
"type": "connection.live",
"payload": { "connection_id": "c3a1…", "waba_id": "2728…" },
"delivery_status": "delivered",
"attempts": 1,
"created_at": "2026-06-01T12:00:00Z"
}
],
"pagination": { "limit": 25, "offset": 0, "has_more": false }
}/v1/tenants/{id}/onboarding-linkReturns the Embedded Signup parameters to drop into your own page. Connections created through it carry this tenant_id automatically.
curl -X POST https://api.ranvedix.com/v1/tenants/t_92…/onboarding-link \
-H "Authorization: Bearer $RANVEDIX_KEY"Response
{
"tenant_id": "t_92…",
"embedded_signup": {
"app_id": "8470…",
"config_id": "1685…",
"graph_version": "v23.0"
},
"exchange_url": "https://api.ranvedix.com/onboarding/exchange"
}Configure a webhook URL + secret under Settings → Platform. We POST a JSON event (e.g. connection.live, quality.changed, heartbeat.lapsing). Each delivery carries an X-Ranvedix-Timestampheader (unix seconds) and an X-Ranvedix-Signature-256 header — an HMAC-SHA256 over <timestamp>.<raw body>. Recompute it and reject any delivery whose timestamp is outside a short tolerance (e.g. 5 minutes) to stop replays.
Verification — Node.js
import crypto from "node:crypto";
const TOLERANCE_S = 300; // reject deliveries older than 5 minutes
function isValid(rawBody, timestamp, header, secret) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > TOLERANCE_S) return false;
const signed = timestamp + "." + rawBody;
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(signed).digest("hex");
return crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}
// Express example
app.post("/webhooks/ranvedix", express.raw({ type: "*/*" }), (req, res) => {
const ok = isValid(
req.body.toString(),
req.headers["x-ranvedix-timestamp"],
req.headers["x-ranvedix-signature-256"],
process.env.RANVEDIX_SIGNING_SECRET,
);
if (!ok) return res.status(401).send("bad signature");
const event = JSON.parse(req.body);
// handle event.type …
res.sendStatus(200);
});Verification — Python
import hashlib, hmac, json, time
TOLERANCE_S = 300 # reject deliveries older than 5 minutes
def is_valid(raw_body: bytes, timestamp: str, header: str, secret: str) -> bool:
if abs(time.time() - int(timestamp)) > TOLERANCE_S:
return False
signed = timestamp.encode() + b"." + raw_body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(header, expected)
# FastAPI example
@app.post("/webhooks/ranvedix")
async def handle(request: Request):
raw = await request.body()
ts = request.headers.get("x-ranvedix-timestamp", "")
sig = request.headers.get("x-ranvedix-signature-256", "")
if not ts or not is_valid(raw, ts, sig, os.environ["RANVEDIX_SIGNING_SECRET"]):
raise HTTPException(status_code=401, detail="bad signature")
event = json.loads(raw)
# handle event["type"] …
return {"ok": True}Event payload schema
{
"id": "evt_01j…", // unique delivery ID
"type": "connection.live", // see event types below
"data": { // type-specific fields
"connection_id": "c3a1…",
"waba_id": "2728…"
},
"created_at": "2026-06-13T12:00:00Z"
}| type | data fields |
|---|---|
connection.created | connection_id, waba_id |
connection.live | connection_id, waba_id |
connection.degraded | connection_id, reason |
quality.changed | connection_id, quality |
heartbeat.expiring | connection_id |
heartbeat.expired | connection_id |
token.expiring | connection_id, days_remaining |
Deliveries retry up to 6 times with exponential backoff. Failed deliveries are logged and replayable from Settings → Webhooks.
Need higher limits, a sandbox tenant, or help integrating? Email hello@ranvedix.com.