Developers / REST API
The HelpVox API lets you register your product catalog and read AI-handled call data. The voice agent answers callers using exactly the data you push here, so keep it in sync with your inventory system (a cron every 15–60 minutes is typical).
Create an API key in Dashboard → Settings and send it as a Bearer token with every request:
Authorization: Bearer hv_live_...All endpoints are scoped to your vendor account. Errors use HTTP status codes and a JSON body: {"error": {"code": "...", "message": "..."}}
1 000 requests per minute per API key, per endpoint — a busy catalog sync never blocks your order polling. Every response carries the standard headers; on 429 wait Retry-After seconds and retry.
X-RateLimit-Limit: 1000 // requests allowed per minute
X-RateLimit-Remaining: 997 // left in the current window
X-RateLimit-Reset: 1765790460 // unix time the window resets
HTTP/2 429 // when exceeded:
Retry-After: 23 // seconds until you may retry
{"error": {"code": "rate_limited", "message": "Rate limit of 1000 requests per minute exceeded. Retry after 23s."}}Creates products. Unlike PUT, an external_id that already exists is an error instead of a silent overwrite — so a repeated import cannot quietly replace a name or a price. Accepts a single product object or the same { products: [...] } envelope as PUT.
curl -X POST https://yourdomain.com/api/v1/products \
-H "Authorization: Bearer hv_live_..." \
-H "Content-Type: application/json" \
-d '{
"external_id": "SKU-1001",
"name": "Trail running shoes X1",
"description": "Lightweight trail shoes with Vibram sole...",
"category": "Shoes",
"brand": "Acme",
"active": true,
"variants": [
{"sku": "SKU-1001-42-BLK", "label": "Size 42, black",
"attributes": {"size": "42", "color": "black"},
"barcode": "3800123456789", "weight_kg": 0.85,
"country_code": "BG", "price": 189.99, "currency": "BGN", "stock_quantity": 12}
]
}'Request fields
Identical to PUT (see below), either as one object or wrapped in products[].
Response 201
{
"results": [
{"external_id": "SKU-1001", "id": "9f4e5b2a-...", "action": "created"}
],
"embedded": 1
}Errors
409 already_exists — one or more external_ids already exist
{"error": {"code": "already_exists",
"external_ids": ["SKU-1001"]}}
422 duplicate_external_id — the same external_id twice in one request
422 validation_error — see detailsNothing is written when the request is rejected — fix the payload and send it again.
Batch upsert (create or update) up to 500 products, keyed by your own external_id (SKU). The variant is what actually sells: it carries the price, the stock, the barcode and the weight. A product is only offered to callers in the countries its variants list, and a product with no variants is always treated as inactive — it has no price, so the agent can neither quote nor order it.
curl -X PUT https://yourdomain.com/api/v1/products \
-H "Authorization: Bearer hv_live_..." \
-H "Content-Type: application/json" \
-d '{
"products": [
{
"external_id": "SKU-1001",
"name": "Trail running shoes X1",
"description": "Lightweight trail shoes with Vibram sole...",
"category": "Shoes",
"brand": "Acme",
"active": true,
"variants": [
{"sku": "SKU-1001-42-BLK", "label": "Size 42, black",
"attributes": {"size": "42", "color": "black"},
"barcode": "3800123456789", "weight_kg": 0.85,
"country_code": "BG", "price": 189.99, "currency": "BGN", "stock_quantity": 12},
{"sku": "SKU-1001-43-BLK", "label": "Size 43, black",
"attributes": {"size": "43", "color": "black"},
"country_code": "BG", "price": 189.99, "currency": "BGN", "stock_quantity": 3},
{"sku": "SKU-1001-42-BLK", "label": "Size 42, black",
"country_code": "RO", "price": 389.99, "currency": "RON", "stock_quantity": 4}
]
}
]
}'Request fields
products[] required, 1–500 items
external_id string, required, 1–128 chars — your model code; the
upsert key that groups the variants. NOT a warehouse
SKU — that lives on each variant below.
name string, required, 1–512 chars
description string | null, optional, ≤10000 chars
category string | null, optional, ≤256 chars
brand string | null, optional, ≤256 chars
attributes object, optional — free-form key/value on the product itself
active boolean, optional, default true
variants[] required, 1–200 — no variants means nothing to sell
sku string, optional, ≤128 chars — the SKU the warehouse
actually ships; empty falls back to external_id, which
is what a product with a single variant usually wants
label string, optional, ≤200 chars — what the agent says out
loud ("Size 42, black"); empty is built from attributes
attributes object, optional — what differs: {"size": "42"}
barcode string | null, optional, ≤64 chars — EAN/GTIN per variant
weight_kg number | null, optional — for courier labels and quotes
country_code string, required, ISO 3166-1 alpha-2 ("BG")
price number, required, ≥0
currency string, required, ISO 4217 ("EUR")
stock_quantity integer | null, optional — null = unlimited
available boolean, optional, default trueThe same variant sold in two countries is two entries with the same sku and different country_code, price and currency. The list replaces every existing variant of the product, so a size you stop selling disappears by simply leaving it out.
The older offers[] field still works and becomes one unnamed variant per country, but it cannot express sizes or colours — prefer variants[].
Response 200
{
"results": [
{"external_id": "SKU-1001", "id": "9f4e5b2a-...", "action": "created"},
{"external_id": "SKU-1002", "id": "b2c81d77-...", "action": "updated"}
],
"embedded": 2
}Validation failures return 422 with {"error": {"code": "validation_error", "details": {...}}}.
Lists your products with their variants. active comes back false for a product without variants, whatever you set. Max limit 500. All list endpoints return a meta object — page with offset until has_more is false.
Response 200
{
"total": 128,
"meta": {"total": 128, "limit": 100, "offset": 0, "has_more": true},
"products": [
{
"id": "9f4e5b2a-...",
"external_id": "SKU-1001",
"name": "Trail running shoes X1",
"description": "Lightweight trail shoes...",
"category": "Shoes",
"brand": "Acme",
"attributes": {},
"active": true,
"updated_at": "2026-08-13T10:15:00.000Z",
"variants": [
{"id": "c71a0f38-...", "sku": "SKU-1001-42-BLK", "label": "Size 42, black",
"attributes": {"size": "42", "color": "black"},
"barcode": "3800123456789", "weight_kg": 0.85,
"country_code": "BG", "price": 189.99, "currency": "BGN",
"stock_quantity": 12, "available": true}
]
}
]
}Fetch a single product by your SKU.
Removes a product (and all its variants) from the catalog. The agent stops offering it immediately.
The AI agent takes orders over the phone and website chat. Your warehouse, fulfilment or ERP system integrates in three steps: pull new orders, push status changes and tracking numbers back, and stream courier checkpoints — so the agent always tells customers where their parcel really is.
Order lifecycle
new (received) → processing (being picked) → prepared (packed, waiting for the courier) → in_transit (courier has it) → delivered. A parcel the courier could not hand over becomes delivery_failed and can go back to in_transit on a second attempt, or to returned. cancelled covers an order stopped before dispatch; returned covers a parcel that came back — refused at the door or sent back after delivery.
The agent sets new (order taken), cancelled (customer cancels before shipping) and return_requested. Your system owns processing, shipped, delivered and returned. Every order carries a short order_number the customer knows — all endpoints below accept it in place of the UUID.
Push an order from your shop / WMS / ERP into HelpVox, so the AI agent answers questions about it (status, tracking, contents) exactly like for orders it took itself. Idempotent on external_ref — repeat POSTs return the existing order. Items are free-form snapshots; when sku matches a catalog product they get linked.
curl -X POST https://www.helpvox.ai/api/v1/orders \
-H "Authorization: Bearer hv_live_..." -H "Content-Type: application/json" \
-d '{
"external_ref": "1393-629",
"customer_name": "Florica Mitrasca",
"customer_phone": "40755780257",
"customer_email": "customer@example.com",
"delivery_address": "str Principală 136, Cig",
"delivery_city": "Cig",
"delivery_street": "str Principală",
"delivery_street_num": "136",
"delivery_other": "entrance A, floor 5, first door left",
"delivery_post_code": "317010",
"address_meta": {"office_id": 1053, "city_id": 19901},
"country_code": "RO",
"currency": "RON",
"courier": "econt",
"courier_service": "econt_door",
"payment_method": "cash_on_delivery",
"payment_status": "pending",
"status": "new",
"shipping_amount": 19.99,
"discount_amount": 5.00,
"vat_rate": 19,
"vat_amount": 17.26,
"cod_amount": 104.99,
"total_weight_kg": 1.45,
"placed_at": "2026-08-18T09:14:00Z",
"scheduled_at": "2026-08-21T14:00:00Z",
"items": [
{"sku": "TB-RO-001", "name": "Комплект TeenBook Romania", "quantity": 1,
"unit_price": 90.00, "discount_amount": 5.00,
"vat_rate": 19, "vat_amount": 13.57, "total_amount": 85.00}
]
}'Request fields
external_ref string, optional, 1–120 chars — YOUR order id; idempotency key
customer_name string, required, 2–200 chars
customer_phone string, required, 5–32 chars — used by the agent to verify the caller
customer_email string, optional, valid email
delivery_address string, required, 5–400 chars — the address as one line
The same address, split. Optional, but it is what lets HelpVox recognise a
returning customer's address across conversations, and what a WMS needs to
create a shipment without re-parsing free text.
delivery_city string, optional — city name only
delivery_quarter string, optional — quarter / neighbourhood name only
delivery_street string, optional — street, number, block, floor, apartment
delivery_street_num string, optional — the number on its own; "14A" and "16" on
the same street are different addresses to us and to the courier
delivery_building string, optional
delivery_entrance string, optional
delivery_floor string, optional
delivery_apartment string, optional
delivery_other string, optional — landmark for the courier ("first door left")
delivery_office string, optional — courier office, when delivering to one
delivery_office_code string, optional — the courier's own id for that office
delivery_post_code string, optional — most couriers require it for door delivery
address_meta object, optional — the courier's own nomenclature ids
(office_id, city_id, street_id…). Stored and returned
untouched; we never interpret them.
country_code string, optional, ISO 3166-1 alpha-2, default "BG"
currency string, required, ISO 4217 — applies to all item prices
payment_method string, optional, default "cash_on_delivery"
payment_status "pending" | "collected" | "paid_out", optional
status "new" | "processing" | "prepared" | "in_transit" |
"delivery_failed" | "delivered" | "cancelled" | "returned"
optional, default "new"
tracking_number string, optional, ≤80 chars
courier string, optional, ≤60 chars — the courier company
courier_service string, optional, ≤60 chars — door or office, e.g.
"econt_office", "speedy_door"; it changes the price and the label
notes string, optional, ≤1000 chars
channel "phone" | "chat" | "web" | "import", optional
placed_at ISO 8601 datetime, optional — when the order was really placed.
Send it for orders imported after the fact, otherwise an order
from three days ago looks like it arrived just now.
scheduled_at ISO 8601 datetime, optional — the delivery slot the customer asked for
Money. Send what you already calculated — nothing here is recomputed.
shipping_amount number, optional — delivery fee
discount_amount number, optional — discount on the whole order
vat_rate number, optional, 0–100
vat_amount number, optional
total_amount number, optional — the order total
cod_amount number, optional — what the courier collects on delivery;
lower than total_amount when part was already paid
total_weight_kg number, optional — for the courier quote and label
items[] required, 1–100
sku string, optional — matched against your catalog VARIANT SKUs,
so send the variant's SKU (size/colour), not the parent's
name string, required, 1–512 chars — snapshot, survives catalog edits
quantity integer, required, 1–500
unit_price number, required, ≥0 — in "currency"
discount_amount number, optional — on this line, in money (convert percentages)
vat_rate number, optional, 0–100 — per line, because one order can mix
rates (in Bulgaria books are 9% and toys 20%) and such an order
cannot be invoiced from a single order-level rate
vat_amount number, optional
total_amount number, optional — defaults to quantity × unit_price − discountsubtotal_amount is always Σ quantity × unit_price. When you omit total_amount it defaults to subtotal + shipping − discount; send your own value whenever you round or price differently, and it is stored as given — we never overwrite it, because you know how you calculated it and we do not.
Response 201 (created)
{
"created": true,
"id": "42134ce2-...",
"order_number": "754489",
"external_ref": "1393-629",
"total": "90.00 RON"
}Response 200 (already exists — same external_ref)
{
"created": false,
"id": "42134ce2-...",
"order_number": "754489",
"external_ref": "1393-629"
}Customers can then ask the agent about the order using either number (yours or ours) + the phone the order was made with.
Poll for orders to fulfil (recommended: every 1–5 minutes). Filter by any status, e.g. ?status=return_requested for returns to process.
Response 200
{
"meta": {"total": 3, "limit": 50, "offset": 0, "has_more": false},
"orders": [
{
"id": "42134ce2-...",
"order_number": "754489",
"external_ref": "1393-629", // null for agent-taken orders
"source": "api", // "agent", "api", or the connected
// system's name, e.g. "bigarena"
"status": "new",
"payment_status": "pending", // null | "pending" | "collected" | "paid_out"
"channel": "chat", // "phone" | "chat"
"customer_name": "Florica Mitrasca",
"customer_phone": "40755780257",
"customer_email": "customer@example.com",
"delivery_address": "str Principală 136, Cig",
"delivery_city": "Cig",
"delivery_quarter": null,
"delivery_street": "str Principală",
"delivery_street_num": "136",
"delivery_building": null,
"delivery_entrance": "A",
"delivery_floor": "5",
"delivery_apartment": "3",
"delivery_other": "first door left",
"delivery_office": null,
"delivery_office_code": null,
"delivery_post_code": "317010",
"address_meta": {"office_id": 1053, "city_id": 19901},
"country_code": "RO",
"payment_method": "cash_on_delivery",
"tracking_number": null,
"courier": "econt",
"courier_service": "econt_door",
"subtotal_amount": "90.00", // always Σ quantity × unit_price
"shipping_amount": "19.99",
"discount_amount": "5.00",
"vat_rate": "19.00",
"vat_amount": "17.26",
"total_amount": "104.99",
"cod_amount": "104.99", // what the courier collects on delivery
"total_weight_kg": "1.450",
"currency": "RON",
"placed_at": "2026-08-18T09:14:00.000Z",
"scheduled_at": "2026-08-21T14:00:00.000Z",
"notes": null,
"cancel_reason": null,
"return_reason": null,
"created_at": "2026-08-13T16:02:11.000Z",
"updated_at": "2026-08-13T16:02:11.000Z",
"items": [
{
"product_id": "9f4e5b2a-...", // null when no catalog match
"variant_id": "c71a0f38-...", // the matched variant, null when none
"sku": "TB-RO-001", // your SKU (snapshot)
"name": "Комплект TeenBook Romania",
"variant": "Size 42, black", // null when the product has one variant
"quantity": 1,
"unit_price": "90.00",
"discount_amount": "5.00",
"vat_rate": "19.00",
"vat_amount": "13.57",
"total_amount": "85.00",
"currency": "RON"
}
]
}
]
}Push fulfilment progress back. The path accepts our short order_number, the order UUID, or your external_ref.
Request fields (any subset, ≥1)
status "processing" | "prepared" | "in_transit" | "delivery_failed" | "delivered" | "cancelled" | "returned"
(the old names "packed", "shipped" and "return_requested" are still accepted
and stored as "prepared", "in_transit" and "returned")
tracking_number string, ≤80 chars
courier string, ≤60 chars — the courier company
courier_service string, ≤60 chars — door or office, e.g. "econt_office"
payment_status "pending" | "collected" | "paid_out" optional, stored but not shown
in the dashboard — HelpVox does not track COD settlement itselfcurl -X PATCH https://www.helpvox.ai/api/v1/orders/1393-629 \
-H "Authorization: Bearer hv_live_..." \
-H "Content-Type: application/json" \
-d '{"status": "in_transit", "tracking_number": "SP123456789BG", "courier": "Speedy", "payment_status": "collected"}'Response 200
{"ok": true, "order_number": "754489", "status": "in_transit"}Response 409 (invalid status transition)
{
"error": {
"code": "invalid_transition",
"message": "Cannot move order from 'cancelled' to 'shipped'. Allowed: none."
}
}Once a tracking number is set, the agent reads it to customers who ask about their order.
Stream courier checkpoints (single event or {"events": [...]} up to 100). Re-sending the same events is safe — duplicates are ignored — so full re-syncs are fine. The agent answers "where is my parcel right now?" with the latest checkpoint.
curl -X POST https://www.helpvox.ai/api/v1/orders/19849/events \
-H "Authorization: Bearer hv_live_..." \
-H "Content-Type: application/json" \
-d '{
"events": [
{"occurred_at": "2026-08-12T08:30:00Z", "description": "Handed to courier", "location": "Sofia warehouse", "source": "speedy"},
{"occurred_at": "2026-08-12T14:05:00Z", "description": "Ready for pickup at office Kyuchuk Parizh", "location": "Plovdiv", "source": "speedy"}
]
}'Event fields
occurred_at string, required, ISO 8601 datetime ("2026-08-12T08:30:00Z")
description string, required, 2–400 chars — spoken to customers nearly verbatim
location string, optional, ≤200 chars
source string, optional, ≤60 chars (e.g. "speedy", "packeta")Response 200
{"ok": true, "received": 2, "inserted": 2}inserted < received means duplicates were skipped (same order + timestamp + description). Write descriptions in the language your customers speak.
Full checkpoint history for an order, oldest first.
Response 200
{
"order_number": "754489",
"events": [
{
"occurred_at": "2026-08-12T08:30:00.000Z",
"description": "Handed to courier",
"location": "Sofia warehouse",
"source": "speedy"
}
]
}Fetch a single order — by our short number, the UUID or your external_ref.
Response 200
{
"id": "42134ce2-...",
"order_number": "754489",
"external_ref": "1393-629",
"source": "api",
"status": "in_transit",
"payment_status": "collected",
"tracking_number": "SP123456789BG",
"courier": "Speedy",
"courier_service": "speedy_door",
"customer_name": "Florica Mitrasca",
"customer_phone": "40755780257",
"delivery_address": "str Principală 136, Cig",
"delivery_city": "Cig",
"delivery_quarter": null,
"delivery_street": "str Principală",
"delivery_street_num": "136",
"delivery_building": null,
"delivery_entrance": "A",
"delivery_floor": "5",
"delivery_apartment": "3",
"delivery_other": "first door left",
"delivery_office": null,
"delivery_office_code": null,
"delivery_post_code": "317010",
"address_meta": {"office_id": 1053, "city_id": 19901},
"subtotal_amount": "90.00",
"shipping_amount": "19.99",
"discount_amount": "5.00",
"vat_rate": "19.00",
"vat_amount": "17.26",
"total_amount": "104.99",
"cod_amount": "104.99",
"total_weight_kg": "1.450",
"placed_at": "2026-08-18T09:14:00.000Z",
"scheduled_at": "2026-08-21T14:00:00.000Z",
"currency": "RON",
"created_at": "2026-08-13T16:02:11.000Z",
"items": [
{
"product_id": "9f4e5b2a-...",
"variant_id": "c71a0f38-...",
"sku": "TB-RO-001",
"name": "Комплект TeenBook Romania",
"variant": "Size 42, black",
"quantity": 1,
"unit_price": "90.00",
"discount_amount": "5.00",
"vat_rate": "19.00",
"vat_amount": "13.57",
"total_amount": "85.00"
}
]
}Integration tips
PUT /api/v1/products — the agent refuses to sell items you mark unavailable.shipped + tracking as one PATCH the moment the label is printed.Lists handled calls and chats, newest first, each with the AI analysis summary.
Response 200
{
"meta": {"total": 42, "limit": 50, "offset": 0, "has_more": false},
"calls": [
{
"id": "6f0f8f9e-...",
"channel": "phone", // "phone" | "chat"
"caller_number": "+359•••123", // masked; null for web chats
"language": "bg",
"status": "completed", // "completed" | "failed"
"duration_seconds": 184,
"started_at": "2026-08-13T09:12:44.000Z",
"analysis": {
"purpose": "Клиентът пита за наличност",
"topic": "Наличност на продукт",
"sentiment": "positive", // "positive" | "neutral" | "negative"
"follow_up_needed": false
}
}
]
}Full call detail: metadata, the complete transcript and the full analysis.
{
"id": "6f0f8f9e-...",
"caller_number": "+3598•••123",
"language": "bg",
"duration_seconds": 184,
"transcript": [
{"role": "agent", "message": "Здравейте! ...", "timeInCallSecs": 0},
{"role": "user", "message": "Здравейте, искам да питам за...", "timeInCallSecs": 4}
],
"analysis": {
"purpose": "Клиентът пита за наличност на обувки",
"topic": "Наличност на продукт",
"summary": "...",
"resolution": "...",
"sentiment": "positive",
"follow_up_needed": false,
"products_mentioned": ["Trail running shoes X1"]
}
}Replaces your entire FAQ with the provided items (full-sync, like the catalog). The agent searches these during calls for policy/company questions. Up to 500 items.
{
"items": [
{"question": "Работите ли с наложен платеж?", "answer": "Да, чрез Еконт и Спиди."},
{"question": "Имате ли физически магазин?", "answer": "Да, в София, бул. Витоша 100."}
]
}Lists your current FAQ entries.
Response 200
{
"items": [
{"id": "c1d2e3f4-...", "question": "Работите ли с наложен платеж?", "answer": "Да, чрез Еконт и Спиди."}
]
}Lists callback requests the AI agent registered when a caller needed a human (complaints, order issues, questions outside the catalog). Integrate this with your CRM or ticketing system, or handle them from the dashboard. Filter with status=pending|done.
Response 200
{
"meta": {"total": 2, "limit": 100, "offset": 0, "has_more": false},
"callbacks": [
{
"id": "a7b8c9d0-...",
"caller_name": "Георги Иванов", // null if not given
"phone_number": "0889123456",
"reason": "Проблем с доставка на поръчка",
"country_code": "BG",
"status": "pending", // "pending" | "done"
"created_at": "2026-08-13T11:14:40.000Z"
}
]
}Mark a callback as handled from your CRM/ticketing system.
curl -X PATCH https://www.helpvox.ai/api/v1/callbacks/a7b8c9d0-... \
-H "Authorization: Bearer hv_live_..." -H "Content-Type: application/json" \
-d '{"status": "done", "outcome": "спокоен, проблемът е решен"}'Response 200
{"ok": true, "id": "a7b8c9d0-...", "status": "done"}Booking/appointment requests the AI agent took (service businesses). Statuses: requested → confirmed → done, or cancelled.
Response 200
{
"meta": {"total": 1, "limit": 50, "offset": 0, "has_more": false},
"appointments": [
{
"id": "b3c4d5e6-...",
"status": "requested",
"channel": "phone",
"customer_name": "Мария Иванова",
"customer_phone": "0888123456",
"service": "Сватбен грим",
"preferred_time": "петък след 14 ч.",
"confirmed_for": null, // ISO datetime once confirmed
"notes": null,
"outcome": null,
"created_at": "2026-08-14T09:12:00.000Z"
}
]
}Move an appointment from your CRM or calendar system. confirmed_for (ISO 8601) is required when confirming.
curl -X PATCH https://www.helpvox.ai/api/v1/appointments/b3c4d5e6-... \
-H "Authorization: Bearer hv_live_..." -H "Content-Type: application/json" \
-d '{"status": "confirmed", "confirmed_for": "2026-08-16T10:00:00+03:00"}'Response 200
{"ok": true, "id": "b3c4d5e6-...", "status": "confirmed"}Returns your vendor profile, plan and voice-minute usage, phone numbers, chat agents and catalog size. Useful as a health-check for your integration.
Response 200
{
"vendor": {
"id": "5253ddef-...",
"name": "Издателство Пайви",
"slug": "paivy",
"status": "active",
"plan": "growth", // null = няма избран план
"plan_status": "active",
"elevenlabs_key_configured": false
},
"usage": {"minutes_used": 42, "minutes_included": 3600},
"numbers": [
{"id": "…", "country_code": "BG", "phone_number": "+1862…", "language": "bg", "status": "active", "agent_provisioned": true}
],
"chat_agents": [
{"id": "…", "language": "bg", "agent_provisioned": true}
],
"product_count": 6
}Everything above is also available as a remote MCP server, so Claude, Codex or Cursor can work with your orders and catalogue directly — no API key, just a sign-in. Connect an AI assistant →
PUT /api/v1/products (or add products in the dashboard).