API Documentation
REST API to sync structured data with your Bravos AI bot.
Introduction
The Webhook API lets you sync structured data (JSON or XML) with a Bravos AI bot. You can perform an initial load from a URL and keep the data updated through HTTP webhooks signed with HMAC-SHA256.
Setup
Before sending webhooks, you need to configure the connection from Integrations > Custom Webhook in the admin panel.
- 1
Create the connection
Give it a descriptive name (e.g. "Clinics", "Inventory") and provide the URL where your data lives in JSON or XML format (max 50 MB). The system will download the data and detect fields automatically.
- 2
Choose the identifier field
Once the connection is created, the system detects fields from your data. Select which one is the unique identifier for each record (like a primary key). For example:
id,ref,sku. This field is required in every item you send via webhook. - 3
Activate and collect your credentials
When you activate, the system processes the initial load from your URL. From that point you have:
- Endpoint — the URL to send webhooks to
- Secret — the key to sign each request with HMAC-SHA256
- Dataset slug and Bot ID — already included in the endpoint
Once activated, you can exclude fields from the admin panel so the chatbot doesn't use them in responses.
Authentication
Every request must include an HMAC-SHA256 signature in the X-Bravos-Signature header. It's computed over the entire body using your secret. The format is hex lowercase.
import hmac, hashlib
secret = "YOUR_SECRET_HERE"
body = b'{"action":"upsert","items":[...]}'
signature = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
# Result: "a1b2c3d4..." (hex lowercase)If the signature doesn't match, the server responds with 401. After 10 failed attempts within a minute, the endpoint is temporarily blocked.
Endpoint
POST https://api.bravos-ai.com/integrations/custom-webhook/{bot_id}/{dataset_slug}/syncThe bot_id and dataset_slug values are generated when creating the connection and are visible in the admin panel.
Required headers
Content-Type: application/json
X-Bravos-Signature: <HMAC-SHA256 hex signature>
Create and update data
Send "action": "upsert" with an array of items. If an item with the same identifier already exists, it gets updated. Otherwise, it gets created.
{
"action": "upsert",
"items": [
{
"ref": "MAD-01",
"name": "Madrid Central Clinic",
"address": "Calle Gran Via 30",
"phone": "+34 910 000 000",
"specialties": ["Dentistry", "Orthopedics", "Dermatology"],
"schedule": {
"weekdays": "09:00 - 20:00",
"saturdays": "10:00 - 14:00"
},
"services": [
{"name": "Dental cleaning", "price": 60, "duration_min": 45},
{"name": "Panoramic X-ray", "price": 35, "duration_min": 15},
{"name": "Dermatology consultation", "price": 80, "duration_min": 30}
]
}
]
}Each item must include the field you chose as identifier when activating the connection (in this example, ref). All other fields are flexible and are detected automatically.
Nested data
- Nested objects are flattened with dots:
schedule.weekdays - Text arrays are joined:
"Dentistry, Orthopedics, Dermatology" - Arrays of objects are indexed as sub-records, enabling the chatbot to filter by sub-fields (e.g. "services under $50")
Delete data
Send "action": "delete" with the identifiers of the records to remove. Deletion includes associated sub-records.
{
"action": "delete",
"items": [
{ "ref": "MAD-01" }
]
}Deleting a nonexistent record won't produce an error. The operation is idempotent.
Responses
Success (200)
{
"ok": true,
"request_id": "req_7a3f1b2c",
"action": "upsert",
"stats": {
"processed": 1,
"created": 1,
"updated": 0,
"skipped": 0,
"children_created": 3,
"children_deleted": 0,
"chunks_regenerated": 1
}
}skipped— item content didn't change (embeddings not regenerated)children_created/deleted— sub-records from arrays of objectschunks_regenerated— texts recomputed for semantic search
Error (4xx/5xx)
{
"detail": "Invalid signature"
}The detail field describes the error. HTTP status codes indicate the category (see error table below).
Error codes
| Code | Cause | What to do |
|---|---|---|
| 400 | Malformed JSON, missing identifier field, or invalid structure | Check payload. Don't retry without fixing. |
| 401 | HMAC signature doesn't match | Verify you're using the correct secret and signing the exact body you send. |
| 403 | Request sent over HTTP instead of HTTPS | Always use HTTPS. |
| 404 | Bot or dataset doesn't exist | Check bot_id and dataset_slug in the panel. |
| 409 | Dataset not activated yet | Complete activation in the admin panel. |
| 413 | Body exceeds 5 MB | Split into smaller batches. |
| 429 | Another webhook is being processed or too many failed attempts | Wait 30 seconds and retry. |
| 500 | Internal server error | Retry with exponential backoff (1s, 2s, 4s, 8s, max 5 attempts). |
Limits
50 MB
Initial load (URL)
5 MB
Body per webhook
500
Items per webhook
HTTPS
Required
Bulk updates: if you need to update more than 500 records via webhook, send them in sequential batches of up to 500 each. The initial URL load doesn't have this limit.
Retries: on 5xx, retry with exponential backoff. On 4xx, fix the payload before retrying. On 429, wait 30 seconds.
Idempotency: sending the same upsert twice won't duplicate data — the second request updates what already exists.
Full example
A complete script that sends data, signs with HMAC, and handles the response.
import hmac, hashlib, json, requests
SECRET = "YOUR_SECRET_HERE"
ENDPOINT = "https://api.bravos-ai.com/integrations/custom-webhook/{BOT_ID}/{DATASET_SLUG}/sync"
clinics = [
{
"ref": "MAD-01",
"name": "Madrid Central Clinic",
"address": "Calle Gran Via 30",
"services": [
{"name": "Dental cleaning", "price": 60},
{"name": "Dermatology consultation", "price": 80},
],
},
{
"ref": "BCN-01",
"name": "Barcelona Eixample Clinic",
"address": "Passeig de Gracia 55",
"services": [
{"name": "General checkup", "price": 50},
{"name": "Physiotherapy", "price": 45},
],
},
]
body = json.dumps({"action": "upsert", "items": clinics}).encode()
signature = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
r = requests.post(ENDPOINT, data=body, headers={
"Content-Type": "application/json",
"X-Bravos-Signature": signature,
}, timeout=30)
data = r.json()
if data.get("ok"):
print(f"Processed: {data['stats']['processed']}")
print(f"Created: {data['stats']['created']}, Updated: {data['stats']['updated']}")
else:
print(f"Error {r.status_code}: {data.get('detail')}")