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. 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. 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. 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}/sync

The 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 objects
  • chunks_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

CodeCauseWhat to do
400Malformed JSON, missing identifier field, or invalid structureCheck payload. Don't retry without fixing.
401HMAC signature doesn't matchVerify you're using the correct secret and signing the exact body you send.
403Request sent over HTTP instead of HTTPSAlways use HTTPS.
404Bot or dataset doesn't existCheck bot_id and dataset_slug in the panel.
409Dataset not activated yetComplete activation in the admin panel.
413Body exceeds 5 MBSplit into smaller batches.
429Another webhook is being processed or too many failed attemptsWait 30 seconds and retry.
500Internal server errorRetry 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')}")