REST API and authentication
This page covers the routes that accept a Voxido API key: the inbound route that creates or updates contacts, and a test route for checking your key.
Base URL: https://api.voxido.ai/api/v1
API keys
| Fact | Detail |
|---|---|
| Format | 43 URL-safe characters, no prefix |
| Header | X-API-Key: <your key> |
| Stored by Voxido | Only a SHA-256 hash and the first 8 characters. The full key is shown once. |
| Expiry | None. A key works until it's revoked. |
| Permissions (scopes) | inbound:write lets a key post to inbound APIs. |
Get a key with inbound:write from the inbound API's panel: Integrations → Custom API → Inbound → open the API → Generate key. Keys from Settings → API keys → Generate key get a different default scope and are refused by inbound APIs. See Create and manage API keys.
POST /inbound/
Creates or updates one contact from a JSON body, using the field mapping you set on the inbound API. Copy the full URL from the inbound API's Endpoint URL field.
curl -X POST "https://api.voxido.ai/api/v1/inbound/3f6c…" \
-H "Content-Type: application/json" \
-H "X-API-Key: $VOXIDO_INBOUND_KEY" \
-d '{"full_name": "Priya Sharma", "mobile": "+919812345678", "service": "Annual check-up"}'Request
| Part | Rule |
|---|---|
X-API-Key header | An active key of the same company, with inbound:write |
| Body | One JSON object, at most 1 MB |
| Fields | Any names. Your mapping decides which become contact fields. |
Success: 200
{
"status": "ok",
"contact_id": "8b1d…",
"result": "created",
"goal_fired": false,
"assignee": null
}| Field | Values |
|---|---|
result | created, updated, or skipped (nothing to match on, such as no email or phone) |
goal_fired | true if the inbound API's After update goal ran |
assignee | matched, not_found, ambiguous, not_in_workspace, or null when you don't map an owner |
Errors
| Status | Body | What to do |
|---|---|---|
401 | {"detail": "Unauthorized"} | Wrong, revoked or wrong-company key, a key without inbound:write, or an unknown endpoint. Voxido doesn't say which, on purpose. |
413 | "Payload exceeds the 1 MB inbound limit" | Send less data |
422 | "Request body must be valid JSON" / "Request body must be a JSON object" | Send one JSON object, not an array |
422 | {"detail": "Missing required mapping fields", "missing_required_fields": [...]} | Send the fields marked Required in the mapping |
422 | {"detail": "Unknown …"} | A routing value didn't match any workspace |
429 | "Rate limit exceeded. Please try again later." | Wait for Retry-After seconds |
Each request is handled on its own. If you retry the same body, Voxido matches the same contact again (by the inbound API's Match existing contact by setting), so a retry updates rather than duplicates. An After update goal, if set, runs again.
POST /api-keys/test-signature
A safe route for checking your key, and your HMAC code, before you go live. It changes nothing.
Key only
curl -X POST "https://api.voxido.ai/api/v1/api-keys/test-signature" \
-H "X-API-Key: $VOXIDO_KEY"Returns 200 with "message": "API key is valid!" and the key's first characters.
Key and signature
Add two headers:
X-Timestamp— the current Unix time in seconds.X-Signature— lowercase hex HMAC-SHA256 of the string"{timestamp}.{raw body}", using your full API key as the secret.
import hashlib, hmac, json, time, requests
key = "YOUR_API_KEY"
body = json.dumps({"hello": "world"}) # sign exactly these bytes
ts = str(int(time.time()))
sig = hmac.new(key.encode(), f"{ts}.{body}".encode(), hashlib.sha256).hexdigest()
r = requests.post(
"https://api.voxido.ai/api/v1/api-keys/test-signature",
data=body, # send the same bytes
headers={"X-API-Key": key, "X-Timestamp": ts, "X-Signature": sig,
"Content-Type": "application/json"},
)
print(r.json())| Result | Response |
|---|---|
| Valid key, valid signature | 200, "message": "API key and signature are valid!" |
| Valid key, wrong signature | 200, "success": false, "message": "Signature mismatch", with your signature and the expected one |
No X-API-Key | 400, "Missing X-API-Key header. Send your API key to test." |
| Unknown or revoked key | 401, "Invalid API key. Make sure you're using the full secret key (not just the prefix)." |
| Timestamp more than 5 minutes off | 401, "Timestamp expired. Server time: …" |
This signature format is only for the test route
Inbound APIs authenticate with the key alone. Webhook triggers use a different signature (HMAC of the raw body, no timestamp). See Webhook triggers: payload and HMAC signing.
