Български

Start here — your first call in 5 minutes

Three steps, no reading required: a key → one request → one webhook. Everything below was executed against production; the responses are real (ids, emails and phones masked).

Step 1 — get a key from the merchant

The merchant opens the velik.ai admin → Settings → 🔌 Integrations → API keys → New key, picks a preset (📦 Fulfilment for a warehouse or WMS, 👁 Read only for reporting, ⚙️ Custom for anything else) and hands you the vk_… key once. The key is the merchant's account: one key = one account, Pro plan and above.

Keep it like a password. Never put it in a browser or a mobile app — /api/v1 has no CORS on purpose.

Step 2 — your first request

curl -sS "https://velik.ai/api/v1/orders?status=new&limit=5" \
  -H "Authorization: Bearer $VELIK_API_KEY"
{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000001",
      "number": 16,
      "order_number": 1546,
      "status": "new",
      "payment_status": "pending",
      "payment_method": "cod",
      "funnel_step": "original",
      "parent_order_id": null,
      "total": 29,
      "delivery_price": null,
      "discount_amount": null,
      "coupon_code": null,
      "refund_amount_cents": null,
      "refunded_at": null,
      "customer": {
        "name": "Тест V. C. V.",
        "email": "bo…@convertbuilder.com",
        "phone": "+359 8•• ••• •12",
        "vat": null
      },
      "shipping": {
        "address": "ул. Те ••• (masked)",
        "city": "София",
        "zip": "1000",
        "method": null,
        "courier_slug": "econt",
        "courier_delivery_mode": "home",
        "courier_office_id": null
      },
      "courier": null,
      "tracking_number": null,
      "tracking_url": null,
      "line_items": [
        {
          "qty": 1,
          "sku": null,
          "kind": "main",
          "name": "Хранителна добавка за сън",
          "product_id": "00000000-0000-4000-8000-000000000002",
          "variant_id": null,
          "total_cents": 2900,
          "delivery_type": "physical",
          "unit_price_cents": 2900
        }
      ],
      "product_name": "Хранителна добавка за сън",
      "product_sku": null,
      "quantity": 1,
      "unit_price": 29,
      "notes": null,
      "admin_notes": null,
      "return_note": null,
      "page_id": "00000000-0000-4000-8000-000000000003",
      "invoice_number": null,
      "shipment": null,
      "created_at": "2026-09-04T14:10:54.525378+00:00",
      "updated_at": "2026-09-05T14:34:46.691317+00:00"
    },
    {
      "id": "00000000-0000-4000-8000-000000000004",
      "number": 15,
      "order_number": 1544,
      "status": "new",
      "payment_status": "pending",
      "payment_method": "cod",
      "funnel_step": "original",
      "parent_order_id": null,
      "total": 41,
      "delivery_price": null,
      "discount_amount": null,
      "coupon_code": null,
      "refund_amount_cents": null,
      "refunded_at": null,
      "customer": {
        "name": "Тест V. C.",
        "email": "bo…@convertbuilder.com",
        "phone": "+359 8•• ••• •12",
        "vat": null
      },
      "shipping": {
        "address": "ул. Те ••• (masked)",
        "city": "София",
        "zip": "1000",
        "method": null,
        "courier_slug": "econt",
        "courier_delivery_mode": "home",
        "courier_office_id": null
      },
      "courier": null,
      "tracking_number": null,
      "tracking_url": null,
      "line_items": [
        {
          "qty": 1,
          "sku": null,
          "kind": "main",
          "name": "Хранителна добавка за сън",
          "product_id": "00000000-0000-4000-8000-000000000002",
          "variant_id": null,
          "total_cents": 2900,
          "delivery_type": "physical",
          "unit_price_cents": 2900
        }
      ],
      "product_name": "Хранителна добавка за сън",
      "product_sku": null,
      "quantity": 1,
      "unit_price": 29,
      "notes": "+ Downsell: Serenity Balance Woman — €12 вместо €39 — €12.00",
      "admin_notes": null,
      "return_note": null,
      "page_id": "00000000-0000-4000-8000-000000000003",
      "invoice_number": null,
      "shipment": {
        "id": "00000000-0000-4000-8000-000000000005",
        "courier_slug": "econt",
        "tracking_number": "1051•••91",
        "status": "cancelled",
        "last_status_label": "Анулирана",
        "last_status_at": "2026-09-05T15:12:02.308+00:00"
      },
      "created_at": "2026-09-04T12:22:17.798464+00:00",
      "updated_at": "2026-09-05T15:12:03.084937+00:00"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJjIjoiMjAyNi0wOS0wNFQxMjoyMjoxNy43OTg0NjQrMDA6MDAiLCJpIjoiMjFlOTI4OTItMGQ0NC00OTE4LWJiNzktMDQ0ZDAxZGYwYzE3In0"
}

Every list is paginated the same way (limit ≤ 200, opaque cursor, has_more), every write accepts X-Velik-Dry-Run: 1 to validate without writing, and every error is { "error": { "code", "message" } } — see Errors and limits.

Step 3 — let velik.ai call you

Subscribe to the events you need (the secret is returned once — it signs every delivery):

curl -sS -X POST "https://velik.ai/api/v1/webhooks" \
  -H "Authorization: Bearer $VELIK_API_KEY" -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/velik-webhook", "events": ["order.created", "order.status_changed", "shipment.status_changed"] }'
{
  "data": {
    "id": "00000000-0000-4000-8000-000000000035",
    "url": "https://velik.ai/api/webhooks/sink",
    "description": "Fulfilment WMS (probe)",
    "events": [
      "order.created",
      "order.updated",
      "order.status_changed",
      "shipment.created",
      "shipment.status_changed"
    ],
    "active": true,
    "secret_prefix": "whsec_efcd8722…",
    "last_triggered_at": null,
    "last_status_code": null,
    "dead_notified_at": null,
    "created_at": "2026-09-05T16:06:29.476938+00:00",
    "updated_at": "2026-09-05T16:06:29.476938+00:00"
  },
  "secret": "<64 hex characters — shown once, store it>"
}

Then POST /api/v1/webhooks/{id}/test sends a test envelope and returns the first delivery attempt with the HTTP code your endpoint answered. Verify X-Velik-Signature with the reference code in Node, Python or PHP; the full contract is in Webhooks.

Where to go next

I want to…read
connect a fulfilment centre / WMS end to end (pull orders → accept → ship → stock)Fulfilment quickstart
know every endpoint, field and statusAPI reference · openapi.v1.yaml
try it in Postman / Insomnia / BrunoPostman collection (generated from the spec); Insomnia and Bruno import openapi.v1.yaml directly
give an AI agent (Claude Desktop, Claude Code) the same accessMCP server
understand errors, rate limits, pagination, dry runOverview §5–§8
know what changed and what we promise not to changeVersioning · Changelog
explain this to the business owner (Bulgarian, no code)За собственика на бизнеса

Copy-paste examples: curl.sh · node.mjs · python.py. Questions: support@velik.ai — see Support.

velik.ai Platform API

Base URL: https://velik.ai · REST: /api/v1/… · MCP: POST /api/mcp · Spec: openapi.v1.yaml (rendered) · Български: README.bg.md

The Platform API gives integrators — fulfilment centres, warehouse systems, ERP/BI tools and AI agents — programmatic access to a merchant's orders, shipments, inventory, products, customers and webhooks. It is the same data and the same operations the merchant sees in the velik.ai admin, exposed through two transports that share one contract:

transportforauth
REST https://velik.ai/api/v1/…your backend, WMS, ERPAuthorization: Bearer vk_…
MCP POST https://velik.ai/api/mcp (JSON-RPC 2.0, Streamable HTTP, no sessions)AI agents (Claude Desktop, Claude Code, any MCP client)the same key
Webhooks (we POST to you)push instead of pollingsigned with a per-subscription secret

Guides: Fulfilment quickstart · Webhooks · MCP · Changelog · Examples (curl, Node, Python, signature verification in Node/Python/PHP).

Every example in these documents was executed against production; the responses shown are the real ones with ids, emails and phones masked.


1. Getting a key

The merchant issues the key (you cannot create keys for another account):

  1. velik.ai admin → Settings → 🔌 Integrations → API keys → New key.
  2. Pick a preset (see §2) or choose scopes one by one, give it a name (e.g. "Acme Fulfilment").
  3. The full key vk_… is shown once. The merchant hands it to you over a secure channel. The list later shows only the prefix (vk_ab12cd34), the scopes and last used.

Rules:

  • One key = one account. The key acts as the account owner for the resources its scopes cover. Team-member permissions do not apply to keys. Nothing a key can do crosses into another account.
  • Up to 10 active keys per account. Revoking is immediate (401 unauthorized from then on).
  • Scopes are fixed at issue time. To change them, the merchant issues a new key and revokes the old one.
  • Keys are for server-to-server use. There is no CORS on /api/v1 — never ship a key inside a browser or a mobile app.

2. Scopes and presets

Scopes are explicit, resource:action. :write does not imply :read — a key with orders:write only can update an order it already knows the id of, but cannot list orders. Ask the merchant for both when you need both.

scopegrantsRESTMCP tools
orders:readlist / read orders (with shipment summary)GET /api/v1/orders, GET /api/v1/orders/{id}list_orders, get_order
orders:writestatus, tracking number/url, courier, internal notesPATCH /api/v1/orders/{id}update_order
shipments:readshipments (waybills), status events, label PDFGET /api/v1/orders/{id}/shipment, GET /api/v1/shipments/{id}, GET /api/v1/shipments/{id}/labelget_shipment
shipments:writecreate / cancel waybills through the merchant's couriersPOST /api/v1/shipments, POST /api/v1/shipments/{id}/cancelcreate_shipment, cancel_shipment
inventory:readstock levels, thresholds, the stock ledgerGET /api/v1/inventory/stock, GET /api/v1/inventory/movementsget_inventory_stock, list_stock_movements
inventory:writedeliveries and correctionsPOST /api/v1/inventory/movementsrecord_stock_movement
products:readcatalog with variants, prices, SKUs, dimensionsGET /api/v1/products, GET /api/v1/products/{id}list_products, get_product
products:writecreate products; change price, SKU, name, description, weight / dimensions, active, thresholds — never stock quantity, cost, images or filesPOST /api/v1/products, PATCH /api/v1/products/{id}create_product, update_product
customers:readcustomers (aggregated from orders), saved segmentsGET /api/v1/customers, GET /api/v1/segmentslist_customers, list_segments
customers:writesegments (create / rename / filters / delete), segment members by email or phone, customer notes — never emails, phones, consents or deletionPOST /api/v1/segments, PATCH/DELETE /api/v1/segments/{id}, POST/DELETE /api/v1/segments/{id}/members, POST /api/v1/customers/{key}/notescreate_segment, update_segment, delete_segment, add_segment_members, remove_segment_members, add_customer_note
webhooks:readsubscriptions and their deliveriesGET /api/v1/webhooks, GET /api/v1/webhooks/{id}/deliverieslist_webhooks, list_webhook_deliveries
webhooks:writecreate / update / delete / test subscriptions, retry deliveriesPOST /api/v1/webhooks, PATCH/DELETE /api/v1/webhooks/{id}, POST /api/v1/webhooks/{id}/test, POST /api/v1/webhook-deliveries/{id}/retrycreate_webhook, update_webhook, delete_webhook, test_webhook
submit_brief, get_status, get_factsthe bridge to МАШИНАТА (MCP only, separate contract — see mcp.md §5)submit_brief, get_status, get_facts

Presets the merchant can pick when issuing:

presetscopestypical user
📦 Fulfilmentorders:read, orders:write, shipments:read, shipments:write, inventory:read, inventory:write, products:read, webhooks:read, webhooks:writefulfilment centre / WMS
👁 Read onlyorders:read, shipments:read, inventory:read, products:read, customers:read, webhooks:readBI, accounting, CRM sync
🔗 МАШИНАТАsubmit_brief, get_status, get_factsthe MASHINATA bridge
⚙️ Customany combinationeveryone else

There is no default preset — the merchant chooses explicitly. Fulfilment and Read-only keys do not include the bridge scopes and do not see the bridge tools. products:write and customers:write are in no preset — the merchant grants them one by one under Custom (an ERP/CRM sync is an explicit decision).

3. Plans

The Platform API is available on the Pro plan and above. A key of a Free account authenticates but every call returns 403 plan_required. If the plan cannot be verified the API fails closed with 403 plan_lookup_failed (retry later). velik.ai staff accounts (superadmin role) bypass only the plan check — never tenancy: their keys still see only their own account.

4. Authentication

GET /api/v1/orders?status=new HTTP/1.1
Host: velik.ai
Authorization: Bearer vk_…
resultstatuserror.code
missing / malformed / revoked key, deactivated account401unauthorized (+ WWW-Authenticate: Bearer realm="velik.ai API")
key lacks the scope of this route403scope_missing (+ error.scope)
Free plan403plan_required
plan lookup failed403plan_lookup_failed
over the rate limit429rate_limited (+ Retry-After)

Recorded: a Fulfilment key calling a customers:read route:

{
  "error": {
    "code": "scope_missing",
    "message": "This key does not have the customers:read scope.",
    "scope": "customers:read"
  }
}

5. Errors

Every error is JSON with a stable snake_case code. Key your handling on error.code; error.message is English prose and may change.

{ "error": { "code": "validation_failed", "message": "id must be a UUID.", "details": { … } } }
codestatuswhen
unauthorized401see §4
scope_missing · plan_required · plan_lookup_failed403see §4
method_not_allowed405wrong HTTP method (Allow header lists the right ones; OPTIONS → 204)
validation_failed400bad input: unknown field, wrong type/enum, non-UUID id (path ids and the page_id / product_id / order_id filters), non-ISO 8601 date (from, to, since), bad cursor; details.allowed lists the accepted fields where relevant
not_found404no such row in this account — including rows that belong to another account (never 403)
conflict409state conflict: live shipment exists, shipment already cancelled/terminal, webhook limit (20), delivery already delivered, webhook inactive, members on a computed segment
courier_error502the courier refused or did not answer (details.reason, details.detail)
migration_pending503feature not enabled on this deployment yet
rate_limited429over 300 req/min for this key
internal_error500our bug — retry, and tell us the X-Deploy-SHA

Two rules worth repeating:

  • Ids are UUIDs. Anything else is rejected before touching the database: GET /api/v1/orders/1546400 validation_failed "id must be a UUID." Order numbers (number, order_number) are not ids.
  • Other accounts' rows are 404, never 403. The API does not reveal whether an id exists elsewhere.
{
  "error": {
    "code": "validation_failed",
    "message": "Unknown field(s): total. Allowed: status, tracking_number, tracking_url, courier, admin_notes, notify_customer.",
    "details": {
      "allowed": [
        "status",
        "tracking_number",
        "tracking_url",
        "courier",
        "admin_notes",
        "notify_customer"
      ]
    }
  }
}

6. Rate limits

transportlimitbucketheaders
REST /api/v1300 requests / minuteper keyX-RateLimit-Limit, X-RateLimit-Remaining, Retry-After (on 429)
MCP /api/mcp120 messages / minute (a JSON-RPC batch of N counts N; max 20 per POST)per keyX-RateLimit-Limit, X-RateLimit-Remaining, Retry-After (on 429)
Webhook test and retry (POST /api/v1/webhooks/{id}/test, POST /api/v1/webhook-deliveries/{id}/retry, the admin buttons, the MCP tools)30 / minute eachper accountRetry-After (on 429)

Back off on 429 for Retry-After seconds. Prefer webhooks (§10) over tight polling.

7. Pagination and polling

List endpoints return a page and an opaque cursor:

{ "data": [ … ], "has_more": true, "next_cursor": "eyJjIjoi…" }
  • limit — default 50, max 200.
  • cursor — pass next_cursor back to get the next page. Cursors are keyset (created_at, id), so new rows inserted while you paginate do not shift the pages. A malformed cursor → 400 validation_failed.
  • Orders support since=<ISO 8601> (updated_at ≥), the cheapest way to poll for changes without webhooks: remember the updated_at of the newest row you processed and ask for everything since. Times are UTC ISO 8601.
  • GET /api/v1/customers is an aggregate and uses limit / offset / total instead.
  • GET /api/v1/segments and GET /api/v1/webhooks return everything (small lists).

8. Dry run (sandbox)

There is no separate sandbox environment. Instead, every write operation accepts

X-Velik-Dry-Run: 1

and then validates everything — ownership, fields, state, courier resolution, stock — but writes nothing and calls no courier. The response carries dry_run: true and describes what would happen. Use it to test your integration against the merchant's real data, and to preview before committing.

{
  "dry_run": true,
  "data": {
    "id": "00000000-0000-4000-8000-000000000001",
    "number": 16,
    "order_number": 1546,
    "status": "new",
    "payment_status": "pending",
    "payment_method": "cod",
    "funnel_step": "original",
    "parent_order_id": null,
    "total": 29,
    "delivery_price": null,
    "discount_amount": null,
    "coupon_code": null,
    "refund_amount_cents": null,
    "refunded_at": null,
    "customer": {
      "name": "Тест V. C. V.",
      "email": "bo…@convertbuilder.com",
      "phone": "+359 8•• ••• •12",
      "vat": null
    },
    "shipping": {
      "address": "ул. Те ••• (masked)",
      "city": "София",
      "zip": "1000",
      "method": null,
      "courier_slug": "econt",
      "courier_delivery_mode": "home",
      "courier_office_id": null
    },
    "courier": null,
    "tracking_number": null,
    "tracking_url": null,
    "line_items": [
      {
        "qty": 1,
        "sku": null,
        "kind": "main",
        "name": "Хранителна добавка за сън",
        "product_id": "00000000-0000-4000-8000-000000000002",
        "variant_id": null,
        "total_cents": 2900,
        "delivery_type": "physical",
        "unit_price_cents": 2900
      }
    ],
    "product_name": "Хранителна добавка за сън",
    "product_sku": null,
    "quantity": 1,
    "unit_price": 29,
    "notes": null,
    "admin_notes": null,
    "return_note": null,
    "page_id": "00000000-0000-4000-8000-000000000003",
    "invoice_number": null,
    "shipment": null,
    "created_at": "2026-09-04T14:10:54.525378+00:00",
    "updated_at": "2026-09-05T14:34:46.691317+00:00"
  },
  "changes": {
    "status": "processing"
  },
  "status_would_change": true,
  "would_notify_customer": false
}

Every response, success or error, carries X-Deploy-SHA — the revision of velik.ai that served it. Quote it when you report a problem.

9. Resources

method & pathscopereturns
GET /api/v1/ordersorders:readpage of Order — filters status, payment, from, to, since, page_id, product_id, courier (none = no tracking number yet), shipment_status, q
GET /api/v1/orders/{id}orders:readOrder
PATCH /api/v1/orders/{id}orders:writestatus, tracking_number, tracking_url, courier, admin_notes, notify_customerOrder + status_changed, email_sent
GET /api/v1/orders/{id}/shipmentshipments:readlatest Shipment of the order or null
POST /api/v1/shipmentsshipments:writecreate a waybill through the merchant's courier → Shipment
GET /api/v1/shipments/{id}shipments:readShipment with status_events
GET /api/v1/shipments/{id}/labelshipments:readthe label as application/pdf
POST /api/v1/shipments/{id}/cancelshipments:writecancel at the courier
GET /api/v1/inventory/stockinventory:readstock per active physical product (+ variants, thresholds)
GET /api/v1/inventory/movementsinventory:readthe stock ledger (sale, release, return, delivery, correction)
POST /api/v1/inventory/movementsinventory:writerecord a delivery or a correction
GET /api/v1/products · GET /api/v1/products/{id}products:readcatalog with variants; ?status=all includes deactivated
POST /api/v1/products · PATCH /api/v1/products/{id}products:writecreate / update catalog fields (price, SKU, name, dimensions, active…) — not stock, cost or media
GET /api/v1/customerscustomers:readcustomers aggregated from orders
GET /api/v1/segmentscustomers:readsaved customer segments
POST /api/v1/segments · PATCH/DELETE /api/v1/segments/{id}customers:writecomputed (filters) or membership segments
POST/DELETE /api/v1/segments/{id}/memberscustomers:writepush / remove customers (email or phone) in a membership segment
POST /api/v1/customers/{key}/notescustomers:writeinternal note on a customer
GET /api/v1/webhooks · POST /api/v1/webhookswebhooks:read / webhooks:writesubscriptions (secret returned once on create)
PATCH /api/v1/webhooks/{id} · DELETE /api/v1/webhooks/{id}webhooks:writeupdate / delete
GET /api/v1/webhooks/{id}/deliveries · POST /api/v1/webhooks/{id}/testwebhooks:read / webhooks:writedeliveries / send a test event
POST /api/v1/webhook-deliveries/{id}/retrywebhooks:writeretry a delivery now

Full parameters, request bodies and response schemas: openapi.v1.yaml / velik.ai/developers/api.

The data model in one screen

  • Orderid (UUID), number (the merchant's per-account order number), statusnew · confirmed · processing · shipped · delivered · cancelled · returned · refunded · archived, payment_status, payment_method (cod, bank, card, free…), money as numbers in the page currency (total, delivery_price, discount_amount), customer { name, email, phone, vat }, shipping { address, city, zip, method, courier_slug, courier_delivery_mode, courier_office_id }, courier / tracking_number / tracking_url (yours or ours), line_items[] (the frozen bill, amounts in cents: kind, product_id, variant_id, name, sku, qty, unit_price_cents, total_cents), shipment (summary of the latest waybill or null), admin_notes, page_id, timestamps. Legacy orders have line_items: null and the single product in product_name / quantity / unit_price. Attribution, IPs, payment-provider ids and costs are never exposed.
  • Shipment — a waybill created through velik.ai: courier_slug (econt / speedy / boxnow), tracking_number, normalized statuspending · picked_up · in_transit · delivered · cancelled · refused · returned, status_events[] ({ at, code, label } — the raw courier events), delivery_mode (office / home), office_id, cod_amount, shipping_cost, parcel weight/dimensions, sender_origin, shipment_terms.
  • Productname, sku, price, active, delivery_type (physical / digital / free), images[] (URLs), stock_quantity (null = not tracked), low_stock_threshold, weight_g, dim_cm { l, w, h }, variants[].
  • Stock itemproduct_id, tracked, stock_quantity, effective_threshold, low, variants[]. Movementtype, signed qty, order_id, note.
  • Customer — an aggregate keyed by email (else phone): orders_count, total_spent, first/last order. Segmentname, filters, customer_count; a segment with filters: { inSegment: <its own id> } is a membership segment (filled through the API or imports), any other filters object is computed from orders. Notecustomer_key, text, created_at.
  • Webhookurl, events[], active, secret_prefix, last_status_code. Deliveryevent, statuspending · delivered · failed · dead, attempt, next_attempt_at, last_status_code.

10. Webhooks in one paragraph

Subscribe to order.created, order.updated, order.status_changed, order.paid, shipment.created, shipment.status_changed, shipment.cancelled, inventory.low_stock. Deliveries are JSON envelopes { id, event, created_at, account_id, api_version, data }, signed with X-Velik-Signature: t=…,v1=… (HMAC-SHA256 over t.body), delivered at least once with retries (1 min → 5 min → 30 min → 2 h → 12 h; 6 attempts), deduplicated by id. Return any 2xx. Details, every payload and verification code in three languages: webhooks.md.

11. Versioning and stability

  • The version is in the path: /api/v1. Webhook envelopes carry api_version (2026-09-01) because there is no path there.
  • v1 changes only additively: new endpoints, new optional fields, new enum values (documented in the CHANGELOG). Fields are never removed, renamed or retyped. Treat unknown fields and unknown enum values as forward-compatible.
  • A breaking change means /api/v2, with v1 kept for at least 12 months in parallel, Deprecation and Sunset headers on v1, a changelog entry and an email to merchants with active keys.
  • The contract is enforced by tests in the velik.ai repository: every route must exist in openapi.v1.yaml, every response must validate against its schema, and a change to the spec requires a changelog entry.

12. Good citizenship

  • Store the key like a password. Rotate by issuing a new key and revoking the old one.
  • Use since or webhooks instead of full re-syncs.
  • Send X-Velik-Dry-Run: 1 when you are unsure.
  • Set a User-Agent that identifies your integration.
  • Support: support@velik.ai — include the X-Deploy-SHA, the path and the error.code.

13. Support and status

  • Where: support@velik.ai. One integration per thread; we answer on business days, production incidents first.
  • What to include: the X-Deploy-SHA from the response, the path and method, the error.code and message, the time (UTC) and — for webhooks — the X-Velik-Delivery / event_id. Never send your key; the vk_ prefix (first 11 characters) is enough to identify it.
  • Platform status: every /api/v1 response carries X-Deploy-SHA (the revision serving you). A 503 migration_pending means a rollout is in progress — retry in a minute. There is no separate status page yet; incidents that affect integrators are announced by email to merchants with active keys.
  • Changes: additive only in v1, always listed in the Changelog with the contract hash. Breaking changes mean /api/v2 with at least 12 months of v1 in parallel (see §11).

Quickstart: connecting a fulfilment centre

Goal: pull new orders, accept them, ship them (with your own tracking number or through the merchant's courier), keep stock in sync, and get told when things change — without polling. Every response below is a real one from production (ids/emails masked). Български: quickstart-fulfilment.bg.md.

You need: a key with the 📦 Fulfilment preset, issued by the merchant (velik.ai → Settings → 🔌 Integrations → API keys). One key per merchant you serve.

export VELIK_API_KEY=vk_…            # from the merchant
export BASE=https://velik.ai

Step 1 — pull the new orders

status=new is the work list. Follow next_cursor until has_more is false.

curl -sS -H "Authorization: Bearer $VELIK_API_KEY" "$BASE/api/v1/orders?status=new&limit=2"
{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000001",
      "number": 16,
      "order_number": 1546,
      "status": "new",
      "payment_status": "pending",
      "payment_method": "cod",
      "funnel_step": "original",
      "parent_order_id": null,
      "total": 29,
      "delivery_price": null,
      "discount_amount": null,
      "coupon_code": null,
      "refund_amount_cents": null,
      "refunded_at": null,
      "customer": {
        "name": "Тест V. C. V.",
        "email": "bo…@convertbuilder.com",
        "phone": "+359 8•• ••• •12",
        "vat": null
      },
      "shipping": {
        "address": "ул. Те ••• (masked)",
        "city": "София",
        "zip": "1000",
        "method": null,
        "courier_slug": "econt",
        "courier_delivery_mode": "home",
        "courier_office_id": null
      },
      "courier": null,
      "tracking_number": null,
      "tracking_url": null,
      "line_items": [
        {
          "qty": 1,
          "sku": null,
          "kind": "main",
          "name": "Хранителна добавка за сън",
          "product_id": "00000000-0000-4000-8000-000000000002",
          "variant_id": null,
          "total_cents": 2900,
          "delivery_type": "physical",
          "unit_price_cents": 2900
        }
      ],
      "product_name": "Хранителна добавка за сън",
      "product_sku": null,
      "quantity": 1,
      "unit_price": 29,
      "notes": null,
      "admin_notes": null,
      "return_note": null,
      "page_id": "00000000-0000-4000-8000-000000000003",
      "invoice_number": null,
      "shipment": null,
      "created_at": "2026-09-04T14:10:54.525378+00:00",
      "updated_at": "2026-09-05T14:34:46.691317+00:00"
    },
    {
      "id": "00000000-0000-4000-8000-000000000004",
      "number": 15,
      "order_number": 1544,
      "status": "new",
      "payment_status": "pending",
      "payment_method": "cod",
      "funnel_step": "original",
      "parent_order_id": null,
      "total": 41,
      "delivery_price": null,
      "discount_amount": null,
      "coupon_code": null,
      "refund_amount_cents": null,
      "refunded_at": null,
      "customer": {
        "name": "Тест V. C.",
        "email": "bo…@convertbuilder.com",
        "phone": "+359 8•• ••• •12",
        "vat": null
      },
      "shipping": {
        "address": "ул. Те ••• (masked)",
        "city": "София",
        "zip": "1000",
        "method": null,
        "courier_slug": "econt",
        "courier_delivery_mode": "home",
        "courier_office_id": null
      },
      "courier": null,
      "tracking_number": null,
      "tracking_url": null,
      "line_items": [
        {
          "qty": 1,
          "sku": null,
          "kind": "main",
          "name": "Хранителна добавка за сън",
          "product_id": "00000000-0000-4000-8000-000000000002",
          "variant_id": null,
          "total_cents": 2900,
          "delivery_type": "physical",
          "unit_price_cents": 2900
        }
      ],
      "product_name": "Хранителна добавка за сън",
      "product_sku": null,
      "quantity": 1,
      "unit_price": 29,
      "notes": "+ Downsell: Serenity Balance Woman — €12 вместо €39 — €12.00",
      "admin_notes": null,
      "return_note": null,
      "page_id": "00000000-0000-4000-8000-000000000003",
      "invoice_number": null,
      "shipment": {
        "id": "00000000-0000-4000-8000-000000000005",
        "courier_slug": "econt",
        "tracking_number": "1051•••91",
        "status": "cancelled",
        "last_status_label": "Анулирана",
        "last_status_at": "2026-09-05T15:12:02.308+00:00"
      },
      "created_at": "2026-09-04T12:22:17.798464+00:00",
      "updated_at": "2026-09-05T15:12:03.084937+00:00"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJjIjoiMjAyNi0wOS0wNFQxMjoyMjoxNy43OTg0NjQrMDA6MDAiLCJpIjoiMjFlOTI4OTItMGQ0NC00OTE4LWJiNzktMDQ0ZDAxZGYwYzE3In0"
}

What to read from an order:

  • id — the UUID you will use in every later call (number is what the merchant calls it: #16).
  • line_items[] — what to pack: product_id, variant_id, sku, name, qty; amounts are in cents. kind: "main" is the product, bump / gift / component are add-ons and bundle parts.
  • customer and shipping — recipient, phone, address, and the courier/mode/office the customer chose at checkout (shipping.courier_slug, courier_delivery_mode, courier_office_id).
  • payment_method: "cod" + total — the cash-on-delivery amount.

Tip: courier=none narrows the list to orders that have no tracking number yet, and since=<ISO time> returns only orders updated after a moment (see README §7).

Step 2 — accept the order

Move it to processing so the merchant sees it is being handled. Dry run first if you like:

curl -sS -X PATCH -H "Authorization: Bearer $VELIK_API_KEY" -H "Content-Type: application/json" \
  -H "X-Velik-Dry-Run: 1" "$BASE/api/v1/orders/$ORDER_ID" -d '{"status":"processing"}'

Then for real:

curl -sS -X PATCH -H "Authorization: Bearer $VELIK_API_KEY" -H "Content-Type: application/json" \
  "$BASE/api/v1/orders/$ORDER_ID" -d '{"status":"processing"}'
{
  "data": {
    "id": "00000000-0000-4000-8000-000000000001",
    "number": 16,
    "order_number": 1546,
    "status": "processing",
    "payment_status": "pending",
    "payment_method": "cod",
    "funnel_step": "original",
    "parent_order_id": null,
    "total": 29,
    "delivery_price": null,
    "discount_amount": null,
    "coupon_code": null,
    "refund_amount_cents": null,
    "refunded_at": null,
    "customer": {
      "name": "Тест V. C. V.",
      "email": "bo…@convertbuilder.com",
      "phone": "+359 8•• ••• •12",
      "vat": null
    },
    "shipping": {
      "address": "ул. Те ••• (masked)",
      "city": "София",
      "zip": "1000",
      "method": null,
      "courier_slug": "econt",
      "courier_delivery_mode": "home",
      "courier_office_id": null
    },
    "courier": null,
    "tracking_number": null,
    "tracking_url": null,
    "line_items": [
      {
        "qty": 1,
        "sku": null,
        "kind": "main",
        "name": "Хранителна добавка за сън",
        "product_id": "00000000-0000-4000-8000-000000000002",
        "variant_id": null,
        "total_cents": 2900,
        "delivery_type": "physical",
        "unit_price_cents": 2900
      }
    ],
    "product_name": "Хранителна добавка за сън",
    "product_sku": null,
    "quantity": 1,
    "unit_price": 29,
    "notes": null,
    "admin_notes": null,
    "return_note": null,
    "page_id": "00000000-0000-4000-8000-000000000003",
    "invoice_number": null,
    "shipment": null,
    "created_at": "2026-09-04T14:10:54.525378+00:00",
    "updated_at": "2026-09-05T16:06:31.958157+00:00"
  },
  "status_changed": true,
  "email_sent": false
}

status_changed: true — the transition happened (and every webhook subscriber received order.status_changed). Sending the same status again is a harmless no-op:

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000001",
    "number": 16,
    "order_number": 1546,
    "status": "shipped",
    "payment_status": "pending",
    "payment_method": "cod",
    "funnel_step": "original",
    "parent_order_id": null,
    "total": 29,
    "delivery_price": null,
    "discount_amount": null,
    "coupon_code": null,
    "refund_amount_cents": null,
    "refunded_at": null,
    "customer": {
      "name": "Тест V. C. V.",
      "email": "bo…@convertbuilder.com",
      "phone": "+359 8•• ••• •12",
      "vat": null
    },
    "shipping": {
      "address": "ул. Те ••• (masked)",
      "city": "София",
      "zip": "1000",
      "method": null,
      "courier_slug": "econt",
      "courier_delivery_mode": "home",
      "courier_office_id": null
    },
    "courier": "Speedy",
    "tracking_number": "WMS-2026-000123",
    "tracking_url": "https://tracking.example-wms.com/WMS-2026-000123",
    "line_items": [
      {
        "qty": 1,
        "sku": null,
        "kind": "main",
        "name": "Хранителна добавка за сън",
        "product_id": "00000000-0000-4000-8000-000000000002",
        "variant_id": null,
        "total_cents": 2900,
        "delivery_type": "physical",
        "unit_price_cents": 2900
      }
    ],
    "product_name": "Хранителна добавка за сън",
    "product_sku": null,
    "quantity": 1,
    "unit_price": 29,
    "notes": null,
    "admin_notes": null,
    "return_note": null,
    "page_id": "00000000-0000-4000-8000-000000000003",
    "invoice_number": null,
    "shipment": null,
    "created_at": "2026-09-04T14:10:54.525378+00:00",
    "updated_at": "2026-09-05T16:06:32.348452+00:00"
  },
  "status_changed": false,
  "email_sent": false
}

Step 3 — ship

Two models. Pick the one that matches your contract with the couriers.

(a) You have your own courier contract — send us the tracking number

One PATCH: tracking number (+ optional tracking URL and courier name) and status: "shipped" together. Setting shipped triggers the merchant's "your order was shipped" email and SMS/Viber to the customer, with your tracking number inside, exactly as if the merchant had done it in the admin. Pass "notify_customer": false to suppress that (we did below, because this was a test order).

curl -sS -X PATCH -H "Authorization: Bearer $VELIK_API_KEY" -H "Content-Type: application/json" \
  "$BASE/api/v1/orders/$ORDER_ID" -d '{
    "tracking_number": "WMS-2026-000123",
    "tracking_url": "https://tracking.example-wms.com/WMS-2026-000123",
    "courier": "Speedy",
    "status": "shipped",
    "notify_customer": false
  }'
{
  "data": {
    "id": "00000000-0000-4000-8000-000000000001",
    "number": 16,
    "order_number": 1546,
    "status": "shipped",
    "payment_status": "pending",
    "payment_method": "cod",
    "funnel_step": "original",
    "parent_order_id": null,
    "total": 29,
    "delivery_price": null,
    "discount_amount": null,
    "coupon_code": null,
    "refund_amount_cents": null,
    "refunded_at": null,
    "customer": {
      "name": "Тест V. C. V.",
      "email": "bo…@convertbuilder.com",
      "phone": "+359 8•• ••• •12",
      "vat": null
    },
    "shipping": {
      "address": "ул. Те ••• (masked)",
      "city": "София",
      "zip": "1000",
      "method": null,
      "courier_slug": "econt",
      "courier_delivery_mode": "home",
      "courier_office_id": null
    },
    "courier": "Speedy",
    "tracking_number": "WMS-2026-000123",
    "tracking_url": "https://tracking.example-wms.com/WMS-2026-000123",
    "line_items": [
      {
        "qty": 1,
        "sku": null,
        "kind": "main",
        "name": "Хранителна добавка за сън",
        "product_id": "00000000-0000-4000-8000-000000000002",
        "variant_id": null,
        "total_cents": 2900,
        "delivery_type": "physical",
        "unit_price_cents": 2900
      }
    ],
    "product_name": "Хранителна добавка за сън",
    "product_sku": null,
    "quantity": 1,
    "unit_price": 29,
    "notes": null,
    "admin_notes": null,
    "return_note": null,
    "page_id": "00000000-0000-4000-8000-000000000003",
    "invoice_number": null,
    "shipment": null,
    "created_at": "2026-09-04T14:10:54.525378+00:00",
    "updated_at": "2026-09-05T16:06:32.348452+00:00"
  },
  "status_changed": true,
  "email_sent": false
}

email_sent tells you whether the shipped email actually went out (customer has an email, notifications enabled, template not disabled by the merchant).

(b) Ship through the merchant's courier integration

If the merchant has Econt / Speedy / BoxNow connected in velik.ai, one call creates the waybill with their account, using the courier/mode/office the customer picked at checkout (override with courier, delivery_mode, office_id, weight_g, dim_cm). Cash-on-delivery is computed server-side. Always dry-run first — it resolves everything except the courier call:

curl -sS -X POST -H "Authorization: Bearer $VELIK_API_KEY" -H "Content-Type: application/json" \
  -H "X-Velik-Dry-Run: 1" "$BASE/api/v1/shipments" -d "{\"order_id\":\"$ORDER_ID\"}"
{
  "dry_run": true,
  "courier": "econt",
  "delivery_mode": "home",
  "office_id": null,
  "weight_g": 500,
  "cod_amount": 41,
  "content_label": "Хранителна добавка за сън",
  "shipment_terms": {
    "cod_type": "cash",
    "open_before": "none",
    "refusal_payer": "sender"
  }
}

Drop the header to create it. The response is the Shipment (tracking number, normalized status, COD, parcel); the order moves to processing and webhooks fire shipment.created. Then:

  • label: GET /api/v1/shipments/{shipment_id}/labelapplication/pdf (this is the real Speedy label of a real shipment, 92 866 bytes):

    curl -sS -H "Authorization: Bearer $VELIK_API_KEY" -o label.pdf "$BASE/api/v1/shipments/$SHIPMENT_ID/label"
  • status: GET /api/v1/shipments/{shipment_id} or GET /api/v1/orders/{order_id}/shipment:

    {
      "data": {
        "id": "00000000-0000-4000-8000-000000000008",
        "order_id": "00000000-0000-4000-8000-000000000009",
        "courier_slug": "speedy",
        "tracking_number": "6372•••83",
        "status": "pending",
        "last_status_label": "Получена информация за пратка",
        "last_status_at": "2026-08-28T11:52:05+00:00",
        "status_events": [
          {
            "at": "2026-08-28T14:52:05+0300",
            "code": "148",
            "label": "Получена информация за пратка"
          }
        ],
        "delivery_mode": "office",
        "office_id": "9289",
        "recipient_address": null,
        "cod_amount": 38,
        "shipping_cost": 4.56,
        "parcel_weight_g": 500,
        "parcel_dim_cm": null,
        "sender_origin": {
          "mode": "address"
        },
        "shipment_terms": {
          "cod_type": "cash",
          "open_before": "none",
          "refusal_payer": "sender"
        },
        "created_at": "2026-08-28T11:52:05.636473+00:00",
        "updated_at": "2026-08-28T12:00:12.689+00:00"
      }
    }
  • cancel: POST /api/v1/shipments/{shipment_id}/cancel (dry run shows whether it still can be):

    {
      "dry_run": true,
      "data": {
        "id": "00000000-0000-4000-8000-000000000008",
        "order_id": "00000000-0000-4000-8000-000000000009",
        "courier_slug": "speedy",
        "tracking_number": "6372•••83",
        "status": "pending",
        "last_status_label": "Получена информация за пратка",
        "last_status_at": "2026-08-28T11:52:05+00:00",
        "status_events": [
          {
            "at": "2026-08-28T14:52:05+0300",
            "code": "148",
            "label": "Получена информация за пратка"
          }
        ],
        "delivery_mode": "office",
        "office_id": "9289",
        "recipient_address": null,
        "cod_amount": 38,
        "shipping_cost": 4.56,
        "parcel_weight_g": 500,
        "parcel_dim_cm": null,
        "sender_origin": {
          "mode": "address"
        },
        "shipment_terms": {
          "cod_type": "cash",
          "open_before": "none",
          "refusal_payer": "sender"
        },
        "created_at": "2026-08-28T11:52:05.636473+00:00",
        "updated_at": "2026-08-28T12:00:12.689+00:00"
      },
      "cancellable": true
    }

If the order already has a live waybill you get 409 conflict with details.shipment; if the courier refuses, 502 courier_error with the courier's reason in details.

Step 4 — let velik.ai tell you what changed (webhooks)

Instead of polling, subscribe once per merchant. The secret comes back only in this response:

curl -sS -X POST -H "Authorization: Bearer $VELIK_API_KEY" -H "Content-Type: application/json" \
  "$BASE/api/v1/webhooks" -d '{
    "url": "https://wms.example.com/velik-webhook",
    "events": ["order.created", "order.updated", "order.status_changed", "shipment.created", "shipment.status_changed"],
    "description": "Fulfilment WMS"
  }'
{
  "data": {
    "id": "00000000-0000-4000-8000-000000000035",
    "url": "https://velik.ai/api/webhooks/sink",
    "description": "Fulfilment WMS (probe)",
    "events": [
      "order.created",
      "order.updated",
      "order.status_changed",
      "shipment.created",
      "shipment.status_changed"
    ],
    "active": true,
    "secret_prefix": "whsec_efcd8722…",
    "last_triggered_at": null,
    "last_status_code": null,
    "dead_notified_at": null,
    "created_at": "2026-09-05T16:06:29.476938+00:00",
    "updated_at": "2026-09-05T16:06:29.476938+00:00"
  },
  "secret": "<64 hex characters — shown once, store it>"
}

Send yourself a test event and look at the delivery log:

curl -sS -X POST -H "Authorization: Bearer $VELIK_API_KEY" "$BASE/api/v1/webhooks/$WEBHOOK_ID/test"
curl -sS -H "Authorization: Bearer $VELIK_API_KEY" "$BASE/api/v1/webhooks/$WEBHOOK_ID/deliveries?limit=10"
{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000039",
      "webhook_id": "00000000-0000-4000-8000-000000000035",
      "event_id": "00000000-0000-4000-8000-000000000040",
      "event": "order.status_changed",
      "status": "delivered",
      "attempt": 1,
      "next_attempt_at": "2026-09-05T16:06:32.358+00:00",
      "last_status_code": 200,
      "last_error": null,
      "delivered_at": "2026-09-05T16:06:32.4+00:00",
      "created_at": "2026-09-05T16:06:32.387958+00:00"
    },
    {
      "id": "00000000-0000-4000-8000-000000000041",
      "webhook_id": "00000000-0000-4000-8000-000000000035",
      "event_id": "00000000-0000-4000-8000-000000000042",
      "event": "order.updated",
      "status": "delivered",
      "attempt": 1,
      "next_attempt_at": "2026-09-05T16:06:32.292+00:00",
      "last_status_code": 200,
      "last_error": null,
      "delivered_at": "2026-09-05T16:06:32.335+00:00",
      "created_at": "2026-09-05T16:06:32.326294+00:00"
    },
    {
      "id": "00000000-0000-4000-8000-000000000043",
      "webhook_id": "00000000-0000-4000-8000-000000000035",
      "event_id": "00000000-0000-4000-8000-000000000044",
      "event": "order.status_changed",
      "status": "delivered",
      "attempt": 1,
      "next_attempt_at": "2026-09-05T16:06:31.99+00:00",
      "last_status_code": 200,
      "last_error": null,
      "delivered_at": "2026-09-05T16:06:32.055+00:00",
      "created_at": "2026-09-05T16:06:32.020957+00:00"
    },
    {
      "id": "00000000-0000-4000-8000-000000000037",
      "webhook_id": "00000000-0000-4000-8000-000000000035",
      "event_id": "00000000-0000-4000-8000-000000000038",
      "event": "test",
      "status": "delivered",
      "attempt": 1,
      "next_attempt_at": "2026-09-05T16:06:30.849+00:00",
      "last_status_code": 200,
      "last_error": null,
      "delivered_at": "2026-09-05T16:06:30.903+00:00",
      "created_at": "2026-09-05T16:06:30.88537+00:00"
    }
  ]
}

This is what arrived at the endpoint when we ran Step 2 — order.status_changed, signed:

{
  "user-agent": "velik.ai-webhooks/1",
  "content-type": "application/json",
  "x-velik-event": "order.status_changed",
  "x-velik-delivery": "00000000-0000-4000-8000-000000000101",
  "x-velik-event-id": "00000000-0000-4000-8000-000000000102",
  "x-velik-signature": "t=1788615989,v1=<hex hmac-sha256>",
  "x-velik-timestamp": "1788615989"
}
{
  "id": "00000000-0000-4000-8000-000000000102",
  "data": {
    "order": {
      "id": "00000000-0000-4000-8000-000000000103",
      "notes": null,
      "total": 29,
      "number": 16,
      "status": "new",
      "courier": null,
      "page_id": "00000000-0000-4000-8000-000000000104",
      "customer": {
        "vat": null,
        "name": "Тест V. C. V.",
        "email": "bo…@convertbuilder.com",
        "phone": "+359 8•• ••• •12"
      },
      "quantity": 1,
      "shipment": null,
      "shipping": {
        "zip": "1000",
        "city": "София",
        "method": null,
        "address": "ул. Те ••• (masked)",
        "courier_slug": "econt",
        "courier_office_id": null,
        "courier_delivery_mode": "home"
      },
      "created_at": "2026-09-04T14:10:54.525378+00:00",
      "line_items": [
        {
          "qty": 1,
          "sku": null,
          "kind": "main",
          "name": "Хранителна добавка за сън",
          "product_id": "00000000-0000-4000-8000-000000000105",
          "variant_id": null,
          "total_cents": 2900,
          "delivery_type": "physical",
          "unit_price_cents": 2900
        }
      ],
      "unit_price": 29,
      "updated_at": "2026-09-05T13:46:29.166283+00:00",
      "admin_notes": null,
      "coupon_code": null,
      "funnel_step": "original",
      "product_sku": null,
      "refunded_at": null,
      "return_note": null,
      "order_number": 1546,
      "product_name": "Хранителна добавка за сън",
      "tracking_url": null,
      "delivery_price": null,
      "invoice_number": null,
      "payment_method": "cod",
      "payment_status": "pending",
      "discount_amount": null,
      "parent_order_id": null,
      "tracking_number": null,
      "refund_amount_cents": null
    },
    "source": "api",
    "status": "new",
    "previous_status": "shipped"
  },
  "event": "order.status_changed",
  "account_id": "00000000-0000-4000-8000-000000000106",
  "created_at": "2026-09-05T13:46:29.189Z",
  "api_version": "2026-09-01"
}
  • order.created → a new order to pack (same data.order shape).
  • order.status_changed with status: "delivered" / "returned" / "cancelled" and source: "courier" → the courier reported the outcome; velik.ai mirrors it onto the order (and for cancelled / returned puts the stock back). Use it to close the order in your WMS.
  • shipment.status_changed → every normalized courier status change of a waybill created through velik.ai, with the raw courier event.

Verify the signature and deduplicate by id — see webhooks.md (Node / Python / PHP snippets).

Step 5 — keep stock in sync

Read the levels (per product, with variants and the low-stock threshold):

curl -sS -H "Authorization: Bearer $VELIK_API_KEY" "$BASE/api/v1/inventory/stock"
{
  "data": [
    {
      "product_id": "00000000-0000-4000-8000-000000000010",
      "name": "Електрическа четка за зъби",
      "sku": null,
      "tracked": true,
      "stock_quantity": 50,
      "low_stock_threshold": null,
      "effective_threshold": 5,
      "low": false,
      "block_on_zero_stock": false,
      "variants": []
    }
  ],
  "default_threshold": 5
}

Goods received → delivery (positive qty, optional unit_cost updates the merchant's weighted average cost). Stock-take differences → correction (signed qty). Sales, releases and returns are written by velik.ai itself when orders move. Dry run:

curl -sS -X POST -H "Authorization: Bearer $VELIK_API_KEY" -H "Content-Type: application/json" \
  -H "X-Velik-Dry-Run: 1" "$BASE/api/v1/inventory/movements" \
  -d "{\"product_id\":\"$PRODUCT_ID\",\"type\":\"delivery\",\"qty\":3,\"unit_cost\":2.5,\"note\":\"PO-4411\"}"
{
  "dry_run": true,
  "product_id": "00000000-0000-4000-8000-000000000010",
  "variant_id": null,
  "type": "delivery",
  "qty": 3,
  "unit_cost": 2.5,
  "enables_tracking": false
}

For real (we then reversed it with a correction of −3, so the merchant's stock is unchanged):

{
  "ok": true,
  "applied": 1,
  "new_cost": null
}

The ledger shows every movement, including the ones velik.ai wrote for sales:

curl -sS -H "Authorization: Bearer $VELIK_API_KEY" "$BASE/api/v1/inventory/movements?product_id=$PRODUCT_ID&limit=3"
{
  "data": [
    {
      "id": "00000000-0000-4000-8000-000000000011",
      "product_id": "00000000-0000-4000-8000-000000000010",
      "variant_id": null,
      "order_id": null,
      "type": "correction",
      "qty": 47,
      "note": "probe low_stock (връщане)",
      "created_at": "2026-09-05T15:09:39.738393+00:00"
    },
    {
      "id": "00000000-0000-4000-8000-000000000012",
      "product_id": "00000000-0000-4000-8000-000000000010",
      "variant_id": null,
      "order_id": null,
      "type": "correction",
      "qty": -47,
      "note": "probe low_stock (временно)",
      "created_at": "2026-09-05T15:09:37.380231+00:00"
    },
    {
      "id": "00000000-0000-4000-8000-000000000013",
      "product_id": "00000000-0000-4000-8000-000000000010",
      "variant_id": null,
      "order_id": null,
      "type": "correction",
      "qty": -3,
      "note": "Инвентаризация (probe, връщане)",
      "created_at": "2026-09-05T14:34:37.42505+00:00"
    }
  ],
  "has_more": true,
  "next_cursor": "eyJjIjoiMjAyNi0wOS0wNVQxNDozNDozNy40MjUwNSswMDowMCIsImkiOiIwNzBkODM2YS1hODRlLTRiMTMtOWRhNS0xNzg5MDZhNDUyYmUifQ"
}

Subscribe to inventory.low_stock to be told (once a day, per product) when a tracked product falls to its threshold.

Step 6 — the catalog

You will want SKUs, weights and dimensions for packing:

curl -sS -H "Authorization: Bearer $VELIK_API_KEY" "$BASE/api/v1/products/$PRODUCT_ID"
{
  "data": {
    "id": "00000000-0000-4000-8000-000000000010",
    "name": "Електрическа четка за зъби",
    "marketing_name": null,
    "sku": null,
    "price": 69,
    "original_price": null,
    "active": true,
    "delivery_type": "physical",
    "offer_type": "bundles",
    "description": "✓ Премахва до 10 пъти повече плака в сравнение с ръчна четка\n✓ Избелва зъбите видимо още след първата седмица на употреба\n✓ Защитава венците с интелигентен сензор за натиск\n✓ 4 режима на почистване за персонализирана грижа\n✓ Издръжлива батерия – до 4 седмици на едно зареждане",
    "images": [
      "https://hfnlrnkqudnwppvdluvy.supabase.co/storage/v1/object/public/page-images/generated/1782681654586-ypu2ppw3wc.jpeg",
      "https://hfnlrnkqudnwppvdluvy.supabase.co/storage/v1/object/public/page-images/generated/1782681689061-38s58mjnmwm.jpeg",
      "https://hfnlrnkqudnwppvdluvy.supabase.co/storage/v1/object/public/page-images/generated/1782681794489-30mmnr31kb3.jpeg"
    ],
    "stock_quantity": 50,
    "block_on_zero_stock": false,
    "low_stock_threshold": null,
    "weight_g": null,
    "dim_cm": null,
    "shipping_short_name": null,
    "shipping_category": null,
    "catalog_hidden": false,
    "language": "bg",
    "variants": [],
    "created_at": "2026-06-28T21:19:29.41932+00:00",
    "updated_at": "2026-09-05T15:15:22.873699+00:00"
  }
}

Things that will bite you if you skip them

  • Ids are UUIDs; #16 is number, not id. A non-UUID id is 400, a UUID from another merchant is 404 (never 403).
  • :write does not include :read. The Fulfilment preset has both; a custom key may not.
  • 300 requests/minute per key. Poll with since, or better, use webhooks.
  • Webhooks are at-least-once. The same event id can arrive twice — store processed ids.
  • Free plan → 403 plan_required. The merchant needs Pro or above.
  • Use X-Velik-Dry-Run: 1 on every write while you develop — it validates against the merchant's real data and writes nothing.

Ready-made code: examples/curl.sh, examples/node.mjs, examples/python.py.

Webhooks

velik.ai POSTs signed JSON to your HTTPS endpoint when something happens in the merchant's account. At-least-once delivery with retries; deduplicate by the event id. Manage subscriptions in the admin (Settings → 🔌 Integrations → Webhooks), through /api/v1/webhooks* (webhooks:read / webhooks:write) or through the MCP tools.

1. Events

eventfires whendata
order.createda customer completes checkout on any of the merchant's pages{ order }
order.updatedorder fields other than the status change — tracking number/url, courier, notes, address (admin or API){ order, changed: [columns], source }
order.status_changedthe order status changes — by the merchant, an API key, the courier status mirror, a payment or an automation{ order, previous_status, status, source }
order.paidthe order is paid (card via Stripe, or marked paid by the merchant){ order, previous_payment_status, source }
shipment.createda waybill is created through velik.ai (admin, API or bulk){ shipment, order_id, source }
shipment.status_changedthe courier reports a new normalized status for a waybill (polled every 15 min for Speedy/Econt; pushed by BoxNow){ shipment, previous_status, status, event: { at, code, label } | null, source: "courier" }
shipment.cancelleda waybill is cancelled{ shipment, order_id, courier_cancelled, courier_confirmed, source }
inventory.low_stockthe daily check finds a tracked product at or below its threshold (once per product per day){ product: { id, name, sku }, stock_quantity, threshold }
testyou press Test / call POST /api/v1/webhooks/{id}/test — not subscribable, always delivered to that one subscription{ message, webhook_id }

order is the same public Order view as GET /api/v1/orders/{id}; shipment is the Shipment view. sourcemerchant · api · courier · payment · automation.

Which events a fulfilment centre needs

  • order.created — new work.
  • order.status_changedprocessing/shipped from the merchant, and delivered / returned / cancelled with source: "courier" when the courier reports the outcome (velik.ai mirrors courier statuses onto the order and restocks on cancelled/returned).
  • shipment.status_changed — the fine-grained courier trail, if you ship through the merchant's couriers.
  • inventory.low_stock — replenishment.

2. The envelope

Every delivery is one event:

{
  "id": "00000000-0000-4000-8000-000000000102",
  "data": {
    "order": {
      "id": "00000000-0000-4000-8000-000000000103",
      "notes": null,
      "total": 29,
      "number": 16,
      "status": "new",
      "courier": null,
      "page_id": "00000000-0000-4000-8000-000000000104",
      "customer": {
        "vat": null,
        "name": "Тест V. C. V.",
        "email": "bo…@convertbuilder.com",
        "phone": "+359 8•• ••• •12"
      },
      "quantity": 1,
      "shipment": null,
      "shipping": {
        "zip": "1000",
        "city": "София",
        "method": null,
        "address": "ул. Те ••• (masked)",
        "courier_slug": "econt",
        "courier_office_id": null,
        "courier_delivery_mode": "home"
      },
      "created_at": "2026-09-04T14:10:54.525378+00:00",
      "line_items": [
        {
          "qty": 1,
          "sku": null,
          "kind": "main",
          "name": "Хранителна добавка за сън",
          "product_id": "00000000-0000-4000-8000-000000000105",
          "variant_id": null,
          "total_cents": 2900,
          "delivery_type": "physical",
          "unit_price_cents": 2900
        }
      ],
      "unit_price": 29,
      "updated_at": "2026-09-05T13:46:29.166283+00:00",
      "admin_notes": null,
      "coupon_code": null,
      "funnel_step": "original",
      "product_sku": null,
      "refunded_at": null,
      "return_note": null,
      "order_number": 1546,
      "product_name": "Хранителна добавка за сън",
      "tracking_url": null,
      "delivery_price": null,
      "invoice_number": null,
      "payment_method": "cod",
      "payment_status": "pending",
      "discount_amount": null,
      "parent_order_id": null,
      "tracking_number": null,
      "refund_amount_cents": null
    },
    "source": "api",
    "status": "new",
    "previous_status": "shipped"
  },
  "event": "order.status_changed",
  "account_id": "00000000-0000-4000-8000-000000000106",
  "created_at": "2026-09-05T13:46:29.189Z",
  "api_version": "2026-09-01"
}
fieldmeaning
idevent id (UUID). The idempotency key — the same event may be delivered more than once; process each id once.
eventthe event name
created_atwhen the event happened (UTC ISO 8601)
account_idthe merchant account (user id) — useful when one endpoint serves several merchants
api_version2026-09-01 — the payload version (date-based; changes only additively)
dataevent-specific, see §1

3. Headers and signature

{
  "user-agent": "velik.ai-webhooks/1",
  "content-type": "application/json",
  "x-velik-event": "order.status_changed",
  "x-velik-delivery": "00000000-0000-4000-8000-000000000101",
  "x-velik-event-id": "00000000-0000-4000-8000-000000000102",
  "x-velik-signature": "t=1788615989,v1=<hex hmac-sha256>",
  "x-velik-timestamp": "1788615989"
}
headervalue
Content-Typeapplication/json
User-Agentvelik.ai-webhooks/1
X-Velik-Eventthe event name
X-Velik-Event-Idthe envelope id
X-Velik-Deliverythe delivery attempt id (different on every retry; shown in the deliveries list)
X-Velik-TimestampUnix seconds when this attempt was signed
X-Velik-Signaturet=<timestamp>,v1=<hex HMAC-SHA256(secret, "<timestamp>.<raw body>")>

The secret is per subscription, 64 hex characters, returned once — in the response of POST /api/v1/webhooks (or the modal in the admin). Later reads show only secret_prefix.

Verify

  1. Read the raw request body (bytes, before any JSON parsing or re-serialization).
  2. Parse t and v1 from X-Velik-Signature.
  3. Compute HMAC-SHA256(secret, t + "." + rawBody) and compare with v1 in constant time.
  4. Reject if |now − t| > 300 s (replay protection). Retries are re-signed with a fresh t, so a legitimate late retry still verifies.
  5. Only then parse the JSON.

Reference implementations (each one accepted the real signatures above and rejected a tampered body):

// Node.js (excerpt)
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex')
const ok = v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
# Python (excerpt)
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(v1, expected)
// PHP (excerpt)
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
$ok = hash_equals($expected, $v1);

4. Responding, retries, dead letters

  • Respond 2xx within 10 seconds. Do the work asynchronously if it takes longer. Anything else — non-2xx, timeout, connection error — counts as a failed attempt.

  • Backoff after a failure (6 attempts in total, ≈ 15 hours):

    attemptdelay before it
    1immediately
    260 s
    3300 s (5 min)
    41800 s (30 min)
    57200 s (2 h)
    643200 s (12 h)
  • The first attempt is made immediately, in the same request that produced the event. If that process dies before the attempt, the once-a-minute worker picks the delivery up after a 60-second grace period (so an in-flight first attempt is never duplicated by the worker).

  • After the 6th failure the delivery becomes dead and the merchant gets one email (per subscription, until a delivery succeeds again). Later events keep being attempted — a dead delivery does not pause the subscription.

  • 410 Gone from your endpoint means unsubscribe me: the delivery is marked dead and the subscription is set active: false without an email.

  • Order of delivery is not guaranteed (retries reorder). Use created_at in the envelope, and the updated_at inside the resource, to reason about recency.

  • Response bodies are stored truncated to 1000 characters and visible to the merchant in the deliveries list — return something useful on errors.

A failed attempt as the merchant/API sees it (this endpoint answered 405), and a manual retry:

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000045",
    "webhook_id": "00000000-0000-4000-8000-000000000046",
    "event_id": "00000000-0000-4000-8000-000000000040",
    "event": "order.status_changed",
    "status": "pending",
    "attempt": 1,
    "next_attempt_at": "2026-09-05T16:07:39+00:00",
    "last_status_code": 405,
    "last_error": null,
    "delivered_at": null,
    "created_at": "2026-09-05T16:06:32.387958+00:00"
  },
  "ok": false,
  "status": 405
}

5. Idempotency

The same id can arrive twice (we retry until we see a 2xx, and a 2xx can be lost on the way back). Keep a table of processed event ids (or use id as a unique key on the row you write) and answer 2xx immediately for a duplicate. X-Velik-Delivery differs per attempt; X-Velik-Event-Id / id does not.

6. Managing subscriptions

actionRESTMCP
list (+ available events)GET /api/v1/webhookslist_webhooks
create — secret returned oncePOST /api/v1/webhooks { url, events[], description? }create_webhook
updatePATCH /api/v1/webhooks/{id} { url?, events?, description?, active? }update_webhook
delete (deliveries go with it)DELETE /api/v1/webhooks/{id}delete_webhook
send a test event, wait for the first attemptPOST /api/v1/webhooks/{id}/testtest_webhook
recent deliveriesGET /api/v1/webhooks/{id}/deliveries?limit=list_webhook_deliveries
retry a delivery nowPOST /api/v1/webhook-deliveries/{id}/retry

Test and retry wait for your endpoint (up to 10 s) and are limited to 30 per minute each, per account (429 rate_limited with Retry-After); they count towards the key's 300/min as well.

Limits and validation: https:// only, no credentials in the URL, and the host must be public — loopback, private (10/8, 172.16/12, 192.168/16, 100.64/10), link-local / metadata (169.254/16), localhost, *.local, *.internal and velik.ai itself are rejected with 400 validation_failed (details.reason: scheme, credentials, host_blocked, private_ip, self_target, dns_private, dns_failed). The hostname is resolved and every address is checked, both when you save the URL and before every delivery; redirects are not followed (a 3xx counts as a failed attempt). http://localhost is not accepted: velik.ai's servers can never reach your machine — use a public tunnel or the sink. At least one event, up to 20 subscriptions per account (409 conflict), unknown event → 400 validation_failed with details.allowed. active: true in a PATCH also clears the dead-letter email flag.

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000035",
    "url": "https://velik.ai/api/webhooks/sink",
    "description": "Fulfilment WMS (probe)",
    "events": [
      "order.created",
      "order.updated",
      "order.status_changed",
      "shipment.created",
      "shipment.status_changed"
    ],
    "active": true,
    "secret_prefix": "whsec_efcd8722…",
    "last_triggered_at": null,
    "last_status_code": null,
    "dead_notified_at": null,
    "created_at": "2026-09-05T16:06:29.476938+00:00",
    "updated_at": "2026-09-05T16:06:29.476938+00:00"
  },
  "secret": "<64 hex characters — shown once, store it>"
}
{
  "data": {
    "id": "00000000-0000-4000-8000-000000000037",
    "webhook_id": "00000000-0000-4000-8000-000000000035",
    "event_id": "00000000-0000-4000-8000-000000000038",
    "event": "test",
    "status": "delivered",
    "attempt": 1,
    "next_attempt_at": "2026-09-05T16:06:30.849+00:00",
    "last_status_code": 200,
    "last_error": null,
    "delivered_at": "2026-09-05T16:06:30.903+00:00",
    "created_at": "2026-09-05T16:06:30.88537+00:00"
  },
  "event_id": "00000000-0000-4000-8000-000000000038"
}

7. Debugging without an endpoint: the sink

While you build, point a subscription at velik.ai's own sink and read what we would have sent:

https://velik.ai/api/webhooks/sink?id=<your webhook id>

The sink accepts deliveries only for an existing, active subscription id, stores the headers (including the signature) and the raw body, and answers 200. Create the subscription with any HTTPS URL first, then PATCH url to the sink URL with its own id. The captured deliveries are visible in the merchant's admin. It is rate limited and capped at 64 KB per delivery — a debugging aid, never a production receiver.

8. Payload reference

order.* events carry the full Order (see README §9); shipment.* carry the Shipment. Recorded from production:

order.updated — tracking fields set through the API (changed lists the columns):

{
  "id": "00000000-0000-4000-8000-000000000108",
  "data": {
    "order": {
      "id": "00000000-0000-4000-8000-000000000103",
      "notes": null,
      "total": 29,
      "number": 16,
      "status": "shipped",
      "courier": null,
      "page_id": "00000000-0000-4000-8000-000000000104",
      "customer": {
        "vat": null,
        "name": "Тест V. C. V.",
        "email": "bo…@convertbuilder.com",
        "phone": "+359 8•• ••• •12"
      },
      "quantity": 1,
      "shipment": null,
      "shipping": {
        "zip": "1000",
        "city": "София",
        "method": null,
        "address": "ул. Те ••• (masked)",
        "courier_slug": "econt",
        "courier_office_id": null,
        "courier_delivery_mode": "home"
      },
      "created_at": "2026-09-04T14:10:54.525378+00:00",
      "line_items": [
        {
          "qty": 1,
          "sku": null,
          "kind": "main",
          "name": "Хранителна добавка за сън",
          "product_id": "00000000-0000-4000-8000-000000000105",
          "variant_id": null,
          "total_cents": 2900,
          "delivery_type": "physical",
          "unit_price_cents": 2900
        }
      ],
      "unit_price": 29,
      "updated_at": "2026-09-05T13:46:29.097395+00:00",
      "admin_notes": null,
      "coupon_code": null,
      "funnel_step": "original",
      "product_sku": null,
      "refunded_at": null,
      "return_note": null,
      "order_number": 1546,
      "product_name": "Хранителна добавка за сън",
      "tracking_url": null,
      "delivery_price": null,
      "invoice_number": null,
      "payment_method": "cod",
      "payment_status": "pending",
      "discount_amount": null,
      "parent_order_id": null,
      "tracking_number": null,
      "refund_amount_cents": null
    },
    "source": "api",
    "changed": [
      "tracking_number",
      "tracking_url",
      "courier"
    ]
  },
  "event": "order.updated",
  "account_id": "00000000-0000-4000-8000-000000000106",
  "created_at": "2026-09-05T13:46:29.116Z",
  "api_version": "2026-09-01"
}

test:

{
  "id": "00000000-0000-4000-8000-000000000110",
  "data": {
    "message": "velik.ai webhook test",
    "webhook_id": "00000000-0000-4000-8000-000000000111"
  },
  "event": "test",
  "account_id": "00000000-0000-4000-8000-000000000106",
  "created_at": "2026-09-05T13:46:20.229Z",
  "api_version": "2026-09-01"
}

order.created — a customer completed checkout (the full Order; shipment is null until a waybill exists):

{
  "id": "00000000-0000-4000-8000-000000000109",
  "data": {
    "order": {
      "id": "00000000-0000-4000-8000-000000000110",
      "notes": null,
      "total": 29,
      "number": 17,
      "status": "new",
      "courier": null,
      "page_id": "00000000-0000-4000-8000-000000000111",
      "customer": {
        "vat": null,
        "name": "Тест V. C. V.",
        "email": "bo…@convertbuilder.com",
        "phone": "+359 8•• ••• •12"
      },
      "quantity": 1,
      "shipment": null,
      "shipping": {
        "zip": "1000",
        "city": "София",
        "method": null,
        "address": "ул. Те ••• (masked)",
        "courier_slug": null,
        "courier_office_id": null,
        "courier_delivery_mode": null
      },
      "created_at": "2026-09-05T15:11:02.897407+00:00",
      "line_items": [
        {
          "qty": 1,
          "sku": null,
          "kind": "main",
          "name": "Хранителна добавка за сън",
          "product_id": "00000000-0000-4000-8000-000000000112",
          "variant_id": null,
          "total_cents": 2900,
          "delivery_type": "physical",
          "unit_price_cents": 2900
        }
      ],
      "unit_price": 29,
      "updated_at": null,
      "admin_notes": null,
      "coupon_code": null,
      "funnel_step": null,
      "product_sku": null,
      "refunded_at": null,
      "return_note": null,
      "order_number": 1562,
      "product_name": "Хранителна добавка за сън",
      "tracking_url": null,
      "delivery_price": null,
      "invoice_number": null,
      "payment_method": "cod",
      "payment_status": "pending",
      "discount_amount": null,
      "parent_order_id": null,
      "tracking_number": null,
      "refund_amount_cents": null
    }
  },
  "event": "order.created",
  "account_id": "00000000-0000-4000-8000-000000000105",
  "created_at": "2026-09-05T15:11:05.194Z",
  "api_version": "2026-09-01"
}

shipment.created — a waybill was created through the merchant's courier (here: Econt, to an office):

{
  "id": "00000000-0000-4000-8000-000000000107",
  "data": {
    "source": "merchant",
    "order_id": "00000000-0000-4000-8000-000000000103",
    "shipment": {
      "id": "00000000-0000-4000-8000-000000000104",
      "status": "pending",
      "order_id": "00000000-0000-4000-8000-000000000103",
      "office_id": "1034",
      "cod_amount": 41,
      "created_at": "2026-09-05T15:11:56.976207+00:00",
      "updated_at": "2026-09-05T15:11:56.976207+00:00",
      "courier_slug": "econt",
      "delivery_mode": "office",
      "parcel_dim_cm": null,
      "sender_origin": {
        "mode": "address"
      },
      "shipping_cost": 5.17,
      "status_events": [],
      "last_status_at": null,
      "shipment_terms": {
        "cod_type": "cash",
        "open_before": "none",
        "refusal_payer": "sender"
      },
      "parcel_weight_g": 500,
      "tracking_number": "1051604256491",
      "last_status_label": null,
      "recipient_address": null
    }
  },
  "event": "shipment.created",
  "account_id": "00000000-0000-4000-8000-000000000105",
  "created_at": "2026-09-05T15:11:57.086Z",
  "api_version": "2026-09-01"
}

shipment.cancelled — the waybill above, cancelled at the courier (courier_confirmed: true — Econt acknowledged):

{
  "id": "00000000-0000-4000-8000-000000000102",
  "data": {
    "source": "merchant",
    "order_id": "00000000-0000-4000-8000-000000000103",
    "shipment": {
      "id": "00000000-0000-4000-8000-000000000104",
      "status": "cancelled",
      "order_id": "00000000-0000-4000-8000-000000000103",
      "office_id": "1034",
      "cod_amount": 41,
      "created_at": "2026-09-05T15:11:56.976207+00:00",
      "updated_at": "2026-09-05T15:11:58.465+00:00",
      "courier_slug": "econt",
      "delivery_mode": "office",
      "parcel_dim_cm": null,
      "sender_origin": {
        "mode": "address"
      },
      "shipping_cost": 5.17,
      "status_events": [
        {
          "at": "2026-09-05T15:11:56.000Z",
          "code": "prepared",
          "label": "Очаква предаване към Еконт"
        },
        {
          "at": "2026-09-05T15:12:02.307Z",
          "code": "merchant_cancel",
          "label": "Анулирана от мърчанта (потвърдено от куриера)"
        }
      ],
      "last_status_at": "2026-09-05T15:12:02.308+00:00",
      "shipment_terms": {
        "cod_type": "cash",
        "open_before": "none",
        "refusal_payer": "sender"
      },
      "parcel_weight_g": 500,
      "tracking_number": "1051604256491",
      "last_status_label": "Анулирана",
      "recipient_address": null
    },
    "courier_cancelled": true,
    "courier_confirmed": true
  },
  "event": "shipment.cancelled",
  "account_id": "00000000-0000-4000-8000-000000000105",
  "created_at": "2026-09-05T15:12:02.402Z",
  "api_version": "2026-09-01"
}

inventory.low_stock — the daily check found a tracked product at or below its threshold:

{
  "id": "00000000-0000-4000-8000-000000000102",
  "data": {
    "product": {
      "id": "00000000-0000-4000-8000-000000000103",
      "sku": null,
      "name": "Електрическа четка за зъби"
    },
    "threshold": 5,
    "stock_quantity": 3
  },
  "event": "inventory.low_stock",
  "account_id": "00000000-0000-4000-8000-000000000104",
  "created_at": "2026-09-05T15:09:39.012Z",
  "api_version": "2026-09-01"
}

order.paid — the merchant marked the order as paid in the admin (source: merchant; card payments arrive with source: payment):

{
  "id": "00000000-0000-4000-8000-000000000202",
  "data": {
    "order": {
      "id": "00000000-0000-4000-8000-000000000203",
      "notes": null,
      "total": 29,
      "number": 16,
      "status": "new",
      "courier": null,
      "page_id": "00000000-0000-4000-8000-000000000204",
      "customer": {
        "vat": null,
        "name": "Тест V. C. V.",
        "email": "bo…@convertbuilder.com",
        "phone": "+359 8•• ••• •12"
      },
      "quantity": 1,
      "shipment": null,
      "shipping": {
        "zip": "1000",
        "city": "София",
        "method": null,
        "address": "ул. Те ••• (masked)",
        "courier_slug": "econt",
        "courier_office_id": null,
        "courier_delivery_mode": "home"
      },
      "created_at": "2026-09-04T14:10:54.525378+00:00",
      "line_items": [
        {
          "qty": 1,
          "sku": null,
          "kind": "main",
          "name": "Хранителна добавка за сън",
          "product_id": "00000000-0000-4000-8000-000000000205",
          "variant_id": null,
          "total_cents": 2900,
          "delivery_type": "physical",
          "unit_price_cents": 2900
        }
      ],
      "unit_price": 29,
      "updated_at": "2026-09-05T16:29:32.854232+00:00",
      "admin_notes": null,
      "coupon_code": null,
      "funnel_step": "original",
      "product_sku": null,
      "refunded_at": null,
      "return_note": null,
      "order_number": 1546,
      "product_name": "Хранителна добавка за сън",
      "tracking_url": null,
      "delivery_price": null,
      "invoice_number": null,
      "payment_method": "cod",
      "payment_status": "paid",
      "discount_amount": null,
      "parent_order_id": null,
      "tracking_number": null,
      "refund_amount_cents": null
    },
    "source": "merchant",
    "previous_payment_status": "pending"
  },
  "event": "order.paid",
  "account_id": "00000000-0000-4000-8000-000000000206",
  "created_at": "2026-09-05T16:29:33.015Z",
  "api_version": "2026-09-01"
}

shipment.status_changed follows the data shape in §1; all JSON schemas are in openapi.v1.yaml under the top-level webhooks key (rendered at velik.ai/developers/api).

MCP server

POST https://velik.ai/api/mcp — a Model Context Protocol server over Streamable HTTP without sessions (JSON-RPC 2.0 in, JSON out). The same vk_ API key, the same scopes, the same operations and views as REST /api/v1 — for AI agents instead of code. 120 JSON-RPC messages / minute per key (a batch of N counts as N; a batch may hold at most 20 messages).

1. Connecting

URLhttps://velik.ai/api/mcp
TransportStreamable HTTP, POST only. GET (SSE stream) and DELETE (sessions) answer 405 — there are no sessions; every POST is self-contained. Batches (JSON arrays) are accepted.
AuthAuthorization: Bearer vk_…401 otherwise
Protocol versions2025-06-18, 2025-03-26, 2024-11-05
Methodsinitialize, notifications/initialized, ping, tools/list, tools/call
Rate limit120 messages/min per key — a batch of N costs N; X-RateLimit-Limit / X-RateLimit-Remaining on every response, Retry-After on 429
Batchup to 20 messages per POST; more → 400 JSON-RPC -32600 Batch too large (max 20)

Claude Desktop (via mcp-remote)

claude_desktop_config.json:

{
  "mcpServers": {
    "velik": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://velik.ai/api/mcp", "--header", "Authorization: Bearer ${VELIK_API_KEY}"],
      "env": { "VELIK_API_KEY": "vk_…" }
    }
  }
}

(On Windows, mcp-remote splits arguments on spaces — use "--header", "Authorization:${AUTH}" with "AUTH": "Bearer vk_…" in env.)

Claude Code

claude mcp add --transport http velik https://velik.ai/api/mcp --header "Authorization: Bearer vk_…"

Any client / curl

curl -sS -X POST https://velik.ai/api/mcp -H "Authorization: Bearer $VELIK_API_KEY" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-agent","version":"1.0"}}}'
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "tools": {
        "listChanged": false
      }
    },
    "serverInfo": {
      "name": "velik.ai",
      "version": "1.0.0"
    },
    "instructions": "velik.ai MCP. Инструментите се показват според обхвата на API ключа (Настройки → API ключове). Мостът към МАШИНАТА: submit_brief (брифът натам), get_status и get_facts (статусът и фактите насам) — одобрението (публикуване / включване / изпращане) е в админа на velik.ai. Платформата (orders/shipments/inventory/products/customers): същите данни и изгледи като REST /api/v1, от план Pro нагоре."
  }
}

2. tools/list — what a key sees

The list is filtered by the key's scopes: a tool is visible only when the key holds its scope. So the same server shows a different toolbox to a Fulfilment key, a Read-only key and a МАШИНАТА key.

curl -sS -X POST https://velik.ai/api/mcp -H "Authorization: Bearer $VELIK_API_KEY" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

Fulfilment preset (17 tools; descriptions truncated here — the live ones are longer):

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "list_orders",
        "description": "Orders of this account (newest first, keyset pagination). Filters: status (new, confirmed, processing, shipped, delivere…"
      },
      {
        "name": "get_order",
        "description": "One order by id (UUID) with its shipment summary. Not found in this account → error NOT_FOUND."
      },
      {
        "name": "get_shipment",
        "description": "Shipment (waybill) by shipment id or by order_id: courier, tracking number, normalized status, status events, COD, parce…"
      },
      {
        "name": "get_inventory_stock",
        "description": "Stock levels of active physical products (tracked quantity, variants, low-stock thresholds and flags). Pass product_id f…"
      },
      {
        "name": "list_stock_movements",
        "description": "Stock ledger (sale, release, return, delivery, correction), newest first. Filters: product_id, order_id, type, from/to."
      },
      {
        "name": "list_products",
        "description": "Catalog: products with variants, prices, SKU, stock, shipping dimensions. Active only unless status=\"all\". Filters: deli…"
      },
      {
        "name": "get_product",
        "description": "One product by id with its variants (also inactive ones — they still appear in orders)."
      },
      {
        "name": "update_order",
        "description": "Update an order: status (through the same path as the admin — \"shipped\" emails/SMS the customer per merchant settings, \"…"
      },
      {
        "name": "record_stock_movement",
        "description": "Record a manual stock movement: delivery (qty > 0, optional unit_cost updates the weighted average cost) or correction (…"
      },
      {
        "name": "create_shipment",
        "description": "Create a waybill for an order through the merchant's own courier integration (Econt, Speedy, BoxNow). Uses the courier/m…"
      },
      {
        "name": "cancel_shipment",
        "description": "Cancel a waybill at the courier (Speedy/Econt confirm; BoxNow has no API — marked locally). Frees the order for a new wa…"
      },
      {
        "name": "list_webhooks",
        "description": "Webhook subscriptions of the account (url, events, active, last delivery status) and the list of available events."
      },
      {
        "name": "create_webhook",
        "description": "Create a webhook subscription. Returns the signing secret ONCE — store it. Events: order.created, order.updated, order.s…"
      },
      {
        "name": "update_webhook",
        "description": "Update a webhook: url, events, description, active."
      },
      {
        "name": "delete_webhook",
        "description": "Delete a webhook subscription (its deliveries go with it)."
      },
      {
        "name": "test_webhook",
        "description": "Send a `test` event to one webhook and return the first delivery attempt (status code)."
      },
      {
        "name": "list_webhook_deliveries",
        "description": "Recent deliveries of one webhook (event, status, attempt, last status code)."
      }
    ]
  }
}

Read-only preset (11 tools):

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "tools": [
      "list_orders",
      "get_order",
      "get_shipment",
      "get_inventory_stock",
      "list_stock_movements",
      "list_products",
      "get_product",
      "list_customers",
      "list_webhooks",
      "list_webhook_deliveries",
      "list_segments"
    ]
  }
}

МАШИНАТА preset sees exactly submit_brief, get_status, get_facts (§5) and none of the above.

The platform tools

toolscopeREST equivalent
list_ordersorders:readGET /api/v1/orders — same filters (status, payment, from, to, since, page_id, product_id, courier, shipment_status, q, limit, cursor)
get_orderorders:readGET /api/v1/orders/{id}
get_shipmentshipments:readGET /api/v1/shipments/{id} (by id) or GET /api/v1/orders/{id}/shipment (by order_id)
get_inventory_stockinventory:readGET /api/v1/inventory/stock
list_stock_movementsinventory:readGET /api/v1/inventory/movements
list_productsproducts:readGET /api/v1/products
get_productproducts:readGET /api/v1/products/{id}
list_customerscustomers:readGET /api/v1/customers
list_segmentscustomers:readGET /api/v1/segments
update_orderorders:writePATCH /api/v1/orders/{id} (+ dry_run: true)
record_stock_movementinventory:writePOST /api/v1/inventory/movements (+ dry_run)
create_shipmentshipments:writePOST /api/v1/shipments (+ dry_run)
cancel_shipmentshipments:writePOST /api/v1/shipments/{id}/cancel (+ dry_run)
list_webhookswebhooks:readGET /api/v1/webhooks
create_webhookwebhooks:writePOST /api/v1/webhooks (secret returned once)
update_webhookwebhooks:writePATCH /api/v1/webhooks/{id}
delete_webhookwebhooks:writeDELETE /api/v1/webhooks/{id}
test_webhookwebhooks:writePOST /api/v1/webhooks/{id}/test
list_webhook_deliverieswebhooks:readGET /api/v1/webhooks/{id}/deliveries
create_productproducts:writePOST /api/v1/products (+ dry_run)
update_productproducts:writePATCH /api/v1/products/{id} (+ dry_run)
create_segmentcustomers:writePOST /api/v1/segments (+ dry_run)
update_segmentcustomers:writePATCH /api/v1/segments/{id} (+ dry_run)
delete_segmentcustomers:writeDELETE /api/v1/segments/{id} (+ dry_run)
add_segment_memberscustomers:writePOST /api/v1/segments/{id}/members (+ dry_run)
remove_segment_memberscustomers:writeDELETE /api/v1/segments/{id}/members (+ dry_run)
add_customer_notecustomers:writePOST /api/v1/customers/{key}/notes (+ dry_run)

products:write / customers:write tools appear only for keys that carry those scopes (no preset does — the merchant grants them under Custom). Write tools take dry_run: true as an argument instead of the X-Velik-Dry-Run header. There is no MCP tool for the PDF label (binary) or for retrying a delivery — use REST.

3. tools/call

Arguments mirror the REST parameters; the result carries the same JSON as REST in structuredContent (and stringified in content[0].text).

curl -sS -X POST https://velik.ai/api/mcp -H "Authorization: Bearer $VELIK_API_KEY" -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"list_orders","arguments":{"status":"new","limit":1}}}'
{
  "jsonrpc": "2.0",
  "id": 4,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"data\":[{\"id\":\"00000000-0000-4000-8000-000000000001\",\"number\":16,\"order_number\":1546,\"status\":\"new\",\"payment_status\":\"pending\",\"payment_method\":\"cod\",\"funnel_step\":\"original\",\"parent_order_id\":null,\"total\":29,\"delivery_price\":null,\"discount_amount\":null,\"coupon_code\":null,\"refund_amount_cents\":null,\"refunded_at\":null,\"customer\":{\"name\":\"Тест V. C. V.\",\"email\":\"bo…@convertbuilder.com\",\"phone\":\"+359 8•• ••• •12\",\"vat\":null},\"shipping\":{\"address\":\"ул. Те ••• (masked)\",\"city\":\"София\",\"zip\":\"1000\",\"method\":null,\"courier_slug\":\"econt\",\"courier_delivery_mode\":\"home\",\"courier_office_id\":null},\"courier\":null,\"tracking_number\":null,\"tracking_url\":null,\"line_items\":[{\"qty\":1,\"sku\":null,\"kind\":\"main\",\"name\":\"Хранителна добавка за сън\",\"product_id\":\"00000000-0000-4000-8000-000000000002\",\"variant_id\":null,\"total_cents\":2900,\"delivery_type\":\"physical\",\"unit_price_cents\":2900}],\"product_name\":\"Хранителна добавка за сън\",\"product_sku\":null,\"quantity\":1,\"unit_price\":29,\"notes\":null,\"admin_notes\":null,\"return_note\":null,\"page_id\":\"00000000-0000-4000-8000-000000000003\",\"invoice_number\":null,\"shipment\":null,\"created_at\":\"2026-09-04T14:10:54.525378+00:00\",\"updated_at\":\"2026-09-05T14:34:46.691317+00:00\"}],\"has_more\":true,\"next_cursor\":\"eyJjIjoiMjAyNi0wOS0wNFQxNDoxMDo1NC41MjUzNzgrMDA6MDAiLCJpIjoiM2FlYzgyNDYtYzliNy00YzY3LTk5NTQtZTgyMmM4MWIwNjU4In0\"}"
      }
    ],
    "structuredContent": {
      "data": [
        {
          "id": "00000000-0000-4000-8000-000000000001",
          "number": 16,
          "order_number": 1546,
          "status": "new",
          "payment_status": "pending",
          "payment_method": "cod",
          "funnel_step": "original",
          "parent_order_id": null,
          "total": 29,
          "delivery_price": null,
          "discount_amount": null,
          "coupon_code": null,
          "refund_amount_cents": null,
          "refunded_at": null,
          "customer": {
            "name": "Тест V. C. V.",
            "email": "bo…@convertbuilder.com",
            "phone": "+359 8•• ••• •12",
            "vat": null
          },
          "shipping": {
            "address": "ул. Те ••• (masked)",
            "city": "София",
            "zip": "1000",
            "method": null,
            "courier_slug": "econt",
            "courier_delivery_mode": "home",
            "courier_office_id": null
          },
          "courier": null,
          "tracking_number": null,
          "tracking_url": null,
          "line_items": [
            {
              "qty": 1,
              "sku": null,
              "kind": "main",
              "name": "Хранителна добавка за сън",
              "product_id": "00000000-0000-4000-8000-000000000002",
              "variant_id": null,
              "total_cents": 2900,
              "delivery_type": "physical",
              "unit_price_cents": 2900
            }
          ],
          "product_name": "Хранителна добавка за сън",
          "product_sku": null,
          "quantity": 1,
          "unit_price": 29,
          "notes": null,
          "admin_notes": null,
          "return_note": null,
          "page_id": "00000000-0000-4000-8000-000000000003",
          "invoice_number": null,
          "shipment": null,
          "created_at": "2026-09-04T14:10:54.525378+00:00",
          "updated_at": "2026-09-05T14:34:46.691317+00:00"
        }
      ],
      "has_more": true,
      "next_cursor": "eyJjIjoiMjAyNi0wOS0wNFQxNDoxMDo1NC41MjUzNzgrMDA6MDAiLCJpIjoiM2FlYzgyNDYtYzliNy00YzY3LTk5NTQtZTgyMmM4MWIwNjU4In0"
    },
    "isError": false
  }
}

A dry-run write:

{
  "jsonrpc": "2.0",
  "id": 7,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"dry_run\":true,\"data\":{\"id\":\"00000000-0000-4000-8000-000000000001\",\"number\":16,\"order_number\":1546,\"status\":\"new\",\"payment_status\":\"pending\",\"payment_method\":\"cod\",\"funnel_step\":\"original\",\"parent_order_id\":null,\"total\":29,\"delivery_price\":null,\"discount_amount\":null,\"coupon_code\":null,\"refund_amount_cents\":null,\"refunded_at\":null,\"customer\":{\"name\":\"Тест V. C. V.\",\"email\":\"bo…@convertbuilder.com\",\"phone\":\"+359 8•• ••• •12\",\"vat\":null},\"shipping\":{\"address\":\"ул. Те ••• (masked)\",\"city\":\"София\",\"zip\":\"1000\",\"method\":null,\"courier_slug\":\"econt\",\"courier_delivery_mode\":\"home\",\"courier_office_id\":null},\"courier\":null,\"tracking_number\":null,\"tracking_url\":null,\"line_items\":[{\"qty\":1,\"sku\":null,\"kind\":\"main\",\"name\":\"Хранителна добавка за сън\",\"product_id\":\"00000000-0000-4000-8000-000000000002\",\"variant_id\":null,\"total_cents\":2900,\"delivery_type\":\"physical\",\"unit_price_cents\":2900}],\"product_name\":\"Хранителна добавка за сън\",\"product_sku\":null,\"quantity\":1,\"unit_price\":29,\"notes\":null,\"admin_notes\":null,\"return_note\":null,\"page_id\":\"00000000-0000-4000-8000-000000000003\",\"invoice_number\":null,\"shipment\":null,\"created_at\":\"2026-09-04T14:10:54.525378+00:00\",\"updated_at\":\"2026-09-05T14:34:46.691317+00:00\"},\"changes\":{\"status\":\"processing\"},\"status_would_change\":true,\"would_notify_customer\":false}"
      }
    ],
    "structuredContent": {
      "dry_run": true,
      "data": {
        "id": "00000000-0000-4000-8000-000000000001",
        "number": 16,
        "order_number": 1546,
        "status": "new",
        "payment_status": "pending",
        "payment_method": "cod",
        "funnel_step": "original",
        "parent_order_id": null,
        "total": 29,
        "delivery_price": null,
        "discount_amount": null,
        "coupon_code": null,
        "refund_amount_cents": null,
        "refunded_at": null,
        "customer": {
          "name": "Тест V. C. V.",
          "email": "bo…@convertbuilder.com",
          "phone": "+359 8•• ••• •12",
          "vat": null
        },
        "shipping": {
          "address": "ул. Те ••• (masked)",
          "city": "София",
          "zip": "1000",
          "method": null,
          "courier_slug": "econt",
          "courier_delivery_mode": "home",
          "courier_office_id": null
        },
        "courier": null,
        "tracking_number": null,
        "tracking_url": null,
        "line_items": [
          {
            "qty": 1,
            "sku": null,
            "kind": "main",
            "name": "Хранителна добавка за сън",
            "product_id": "00000000-0000-4000-8000-000000000002",
            "variant_id": null,
            "total_cents": 2900,
            "delivery_type": "physical",
            "unit_price_cents": 2900
          }
        ],
        "product_name": "Хранителна добавка за сън",
        "product_sku": null,
        "quantity": 1,
        "unit_price": 29,
        "notes": null,
        "admin_notes": null,
        "return_note": null,
        "page_id": "00000000-0000-4000-8000-000000000003",
        "invoice_number": null,
        "shipment": null,
        "created_at": "2026-09-04T14:10:54.525378+00:00",
        "updated_at": "2026-09-05T14:34:46.691317+00:00"
      },
      "changes": {
        "status": "processing"
      },
      "status_would_change": true,
      "would_notify_customer": false
    },
    "isError": false
  }
}

4. Errors

Protocol errors are JSON-RPC error objects (-32700 parse, -32600 invalid request, -32601 unknown method, -32602 unknown tool / bad params, -32603 internal). Domain refusals are tool results with isError: true and structuredContent: { error: <CODE>, reason }:

errormeaning
SCOPEthe key lacks the tool's scope (the tool would not be in tools/list either)
PLANFree plan / plan could not be verified
VALIDATION_FAILED, NOT_FOUND, CONFLICT, COURIER_ERROR, …the REST error.code, upper-cased; reason is the message

A Fulfilment key calling a bridge tool, and a lookup of an id that is not in the account:

{
  "jsonrpc": "2.0",
  "id": 5,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Ключът няма обхват submit_brief"
      }
    ],
    "structuredContent": {
      "error": "SCOPE",
      "reason": "Ключът няма обхват submit_brief"
    },
    "isError": true
  }
}
{
  "jsonrpc": "2.0",
  "id": 6,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "Order not found."
      }
    ],
    "structuredContent": {
      "error": "NOT_FOUND",
      "reason": "Order not found."
    },
    "isError": true
  }
}

5. The bridge to МАШИНАТА

The same server also hosts the bridge — three tools with their own contract, used by МАШИНАТА to hand marketing briefs to velik.ai's AI and read back status and facts:

toolscopewhat it does
submit_briefsubmit_briefsubmits a brief (kind: PAGE, AUTOMATION, CAMPAIGN, SEGMENT); returns brief_id + status; idempotent per (business_id, operation, step, run). business_id is optional — it defaults to the key's account. The bridge stops at a draft: publishing / enabling / sending happens in the velik.ai admin.
get_statusget_statusRECEIVED → IN_WORK → AWAITING_APPROVAL → APPROVED → LIVE, or REJECTED / FAILED (with reason, fail_code); admin_url, and on LIVE the asset_id / public_url.
get_factsget_factscounts with provenance for an asset in a from/to window: page_visits, purchases, registrations, attendees, segment_size.

These tools are visible only to keys issued with the МАШИНАТА preset (or a custom key holding those scopes); Fulfilment and Read-only keys neither see nor can call them (SCOPE). The bridge's request/response schemas are a separate, frozen contract shared with МАШИНАТА partners — the tool inputSchemas returned by tools/list are authoritative.

6. Notes for agent builders

  • Read tools are safe to call freely (within 120 messages/min; a batch of N costs N, max 20 per POST). Write tools change real orders and stock and can email/SMS the merchant's customers (update_order with status: "shipped") — prefer dry_run: true first and confirm with the user.
  • Ids are UUIDs; order numbers are not ids. NOT_FOUND also means "not in this account".
  • list_orders / list_stock_movements / list_products return next_cursor — pass it back as cursor.
  • Amounts: total etc. are numbers in the page currency; line_items[].unit_price_cents are cents.

Changelog — velik.ai Platform API

The contract is /api/v1 + the MCP server + webhooks. Only additive changes land in v1; each entry lists them. The Contract hash is the SHA-256 of the canonical openapi.v1.yaml (node scripts/contract-hash.js); a test fails when the spec changes without a new entry here.

2026-09-05 — Webhook test/retry limit, first-attempt grace, share image (security audit)

Contract hash: fa360ce87f03bdc8b07dfebcb8f6286df4fe0c6e231be4f1b43ed04ccdded6b6

  • Webhook test and retry (POST /api/v1/webhooks/{id}/test, POST /api/v1/webhook-deliveries/{id}/retry, the admin buttons and the MCP tools) are limited to 30 per minute each, per account429 rate_limited with Retry-After and details.retry_after_sec. x-velik-rate-limits.webhook_test_retry_per_minute documents it.
  • A delivery's first attempt is still immediate; the once-a-minute worker now waits a 60-second grace period before taking over a delivery whose first attempt may be in flight, so the same event is not sent twice by both. Deduplicate by id regardless (at-least-once stays the contract).
  • /developers pages carry an og:image (1200×630) for link previews.

2026-09-05 — Webhook URL policy and MCP batch limits (security audit)

Contract hash: f19bd466eb731ab19f8ff1d225f72b95f103290e3abd2c9434a088b18760a93b

Two tightenings that a correctly built integration never notices:

  • Webhook URLs (POST/PATCH /api/v1/webhooks, create_webhook / update_webhook): https:// only, no credentials, and the host must be public — loopback, private, link-local / metadata addresses, localhost, *.local, *.internal and velik.ai itself are rejected with 400 validation_failed and details.reason (scheme, credentials, host_blocked, private_ip, self_target, dns_private, dns_failed). The hostname is resolved and checked when you save it and before every delivery. Redirects are no longer followed (a 3xx is a failed attempt, last_error: redirect_not_followed). The former http://localhost exception is removed — velik.ai's servers can never reach your machine; use a public tunnel or the sink.
  • MCP rate limit is now 120 JSON-RPC messages / minute per key: a batch of N costs N, and a batch may hold at most 20 messages (more → 400 JSON-RPC -32600 Batch too large (max 20)). Every /api/mcp response carries X-RateLimit-Limit / X-RateLimit-Remaining like REST; 429 keeps its body { error, retryAfterSec } and adds Retry-After. x-velik-rate-limits.mcp_max_batch documents the cap.

2026-09-05 — Stricter input validation (security audit)

Contract hash: 6c5c40cc1243d07d491fc6042e9af57c0677b4906d2632a6045069a43eed8619

Spec: documentation-only changes (no field, path or status added or removed) — the spec now passes the Spectral oas ruleset with 0 warnings: the description properties of Product / Webhook were written as strings instead of schemas, the rate-limit headers of GET /api/v1/orders are declared per header, error.details declares items, and 13 operations gained a description. New: Postman collection generated from the spec; the Redoc reference is loaded with Subresource Integrity.

Inputs that used to reach the database and fail with 500 internal_error are now rejected up front with 400 validation_failed:

  • Filter ids must be UUIDs: page_id, product_id on GET /api/v1/orders; product_id, order_id on GET /api/v1/inventory/movements; product_id on GET /api/v1/inventory/stock (and the same MCP tools).
  • Date filters must be strict ISO 8601 (2026-09-01, 2026-09-01T10:00:00Z, 2026-09-01T10:00:00.123456+00:00): from, to, since, and the created_at inside a cursor. Lenient forms such as Sep 1, 2026 are rejected.
  • delivery_type on GET /api/v1/products must be physical or digital.
  • last_used_at on an API key is now updated at most once per minute (it used to be written on every request).

2026-09-05 — products:write and customers:write

Contract hash: 09080b2b08a017d65f6094ec1333441a20b42ee9ebbd263f9ead4c51557c99af

Additive. Two new scopes (granted only under the Custom preset; the Fulfilment / Read-only / МАШИНАТА presets are unchanged):

  • products:writePOST /api/v1/products, PATCH /api/v1/products/{id}: name, marketing_name, sku, price, original_price, active, description, delivery_type, weight_g, dim_cm, shipping_short_name, shipping_category, low_stock_threshold, block_on_zero_stock, catalog_hidden. Never stock quantity (use inventory movements), cost, images or files. MCP: create_product, update_product.
  • customers:writePOST /api/v1/segments, PATCH /api/v1/segments/{id}, DELETE /api/v1/segments/{id}, POST /api/v1/segments/{id}/members, DELETE /api/v1/segments/{id}/members, POST /api/v1/customers/{key}/notes. Membership segments (no filters) are filled from a CRM by email / phone; computed segments refuse members with 409 conflict. Never emails, phones, consents or customer deletion. MCP: create_segment, update_segment, delete_segment, add_segment_members, remove_segment_members, add_customer_note.
  • New path parameter key (customer key) and schemas ProductCreate, ProductPatch, SegmentCreate, SegmentPatch, SegmentFilters, SegmentMembersBody, CustomerNoteCreate (+ result / dry-run shapes). All write routes honour X-Velik-Dry-Run.

2026-09-05 — v1 initial release

Contract hash: 60255ea16f2504a5d3a5dfa3f193368f6400cd7a65b23561eabaebdd033537b1

REST /api/v1

  • Orders: GET /api/v1/orders (filters status, payment, from, to, since, page_id, product_id, courier, shipment_status, q; keyset pagination), GET /api/v1/orders/{id}, PATCH /api/v1/orders/{id} (status, tracking_number, tracking_url, courier, admin_notes, notify_customer), GET /api/v1/orders/{id}/shipment.
  • Shipments: POST /api/v1/shipments, GET /api/v1/shipments/{id}, GET /api/v1/shipments/{id}/label (PDF), POST /api/v1/shipments/{id}/cancel.
  • Inventory: GET /api/v1/inventory/stock, GET /api/v1/inventory/movements, POST /api/v1/inventory/movements (delivery / correction).
  • Products: GET /api/v1/products, GET /api/v1/products/{id}.
  • Customers: GET /api/v1/customers (offset pagination), GET /api/v1/segments.
  • Webhooks: GET /api/v1/webhooks, POST /api/v1/webhooks, PATCH /api/v1/webhooks/{id}, DELETE /api/v1/webhooks/{id}, GET /api/v1/webhooks/{id}/deliveries, POST /api/v1/webhooks/{id}/test, POST /api/v1/webhook-deliveries/{id}/retry.
  • Cross-cutting: Bearer vk_ keys with explicit scopes (13; presets Fulfilment / Read only / МАШИНАТА / Custom), Pro+ plan gate, 300 req/min per key with X-RateLimit-*, X-Velik-Dry-Run, { error: { code, message, details? } }, X-Deploy-SHA.

MCP POST /api/mcp

  • 19 platform tools mirroring the REST operations, shown per key scope in tools/list, next to the 3 bridge tools (submit_brief, get_status, get_facts). 120 req/min per key.

Webhooks

  • Events order.created, order.updated, order.status_changed, order.paid, shipment.created, shipment.status_changed, shipment.cancelled, inventory.low_stock (+ test).
  • Envelope { id, event, created_at, account_id, api_version: "2026-09-01", data }, X-Velik-Signature: t=…,v1=… (HMAC-SHA256), at-least-once delivery with backoff 1m · 5m · 30m · 2h · 12h (6 attempts), dead + email, 410 Gone unsubscribes.
  • Replaces the pre-2026-09 flat order.created POST (no subscribers existed).

Generated from docs/integrations/ in the velik.ai repository. Contract: /openapi.v1.yaml · API reference · Postman collection · Privacy · Terms