Developers
Build on Vepra AI
Every action a person takes in the app is a named command with a schema and a permission. The same commands are called with an API key, from webhooks and by the agent. There is no second API.
API keys
A key is a "service" actor of the business with the permissions you pick when creating it (only from your own, never members.manage). Create it under Business → Agent & API keys; the token shows once and only its hash is stored.
A key reaches only its own business (any other route answers 403), expires when you say so and is revoked with one click. It has no MFA: commands that require a fresh code are always refused.
Calling a command
curl -X POST https://vepra.ai/v1/businesses/<BIZNESI>/commands/contact.save \
-H "Authorization: Bearer vak_..." \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{"displayName":"Arta Leka","kind":"person","email":"arta@shembull.al","phone":"+355691234567"}'The receipt
{
"operationId": "3f1c...", "command": "contact.save", "status": "applied",
"resources": [{ "type": "Contact", "id": "a51d...", "version": 1 }],
"eventIds": ["..."], "correlationId": "...", "warnings": []
}Statuses: applied, awaiting_approval (approval inside the platform), pending_external (waiting for an outside party). Errors: 401 unknown or expired key, 403 outside the permissions or the business, 409 stale version (expectedVersion), 422 invalid input with fieldErrors.
Idempotency-Key: the same value twice returns the same receipt without repeating the action.
Commands
The full list with input schemas is in the OpenAPI document: /v1/openapi.json (command route: POST /v1/businesses/{businessId}/commands/{name}). Schemas are named Input_<command with underscores>, e.g. Input_contact_save.
Most used: contact.save, lead.save, booking.hold / booking.confirm, quote.draft / quote.send, task.create, work.create, consent.record.
Reads use the same GET routes the app uses, e.g. GET /v1/businesses/{businessId}/contacts?limit=50, with the matching .read permission.
Inbound webhooks
A public address your systems (forms, payments, Activepieces, n8n, Zapier) POST to. Create it on the same page: a name, what it does (only start workflows, or run a templated command) and the secret, shown once.
The sender proves the secret with X-Vepra-Signature: sha256=<HMAC-SHA256 of the body> or, when it cannot sign, with X-Vepra-Token: <secret>. No proof: 401. Unknown or disabled address: 404. Refused calls are never recorded.
A call with the token
curl -X POST https://vepra.ai/v1/hooks/<HOOK_ID> \
-H "X-Vepra-Token: vhk_..." \
-H "Content-Type: application/json" \
-d '{"name":"Arta Leka","email":"arta@shembull.al","phone":"+355691234567"}'Signing (Node.js)
import { createHmac } from 'node:crypto';
const signature = 'sha256=' + createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
// -> header X-Vepra-Signature: sha256=...The command template
{ "displayName": "{{name}}", "kind": "person", "email": "{{email}}", "phone": "{{phone}}", "notes": "Nga {{source}}" }{{field.subfield}} takes its value from the delivery body; a whole value keeps its type (number, list), a missing value drops the field. Every delivery raises WebhookReceived, a workflow trigger, with contactId when the command produced a contact.
Outbound webhooks
Vepra sends your business events to a public https address you give: contact saved, lead, booking confirmed, quote accepted, work created… (all, or only the ones you pick). Private addresses (10.x, 192.168.x, localhost) are refused.
Each delivery is a JSON POST with three headers: X-Vepra-Event, X-Vepra-Delivery and X-Vepra-Signature (sha256=HMAC-SHA256 of the body with the secret you got once). Answer 2xx within 10 seconds; redirects are not followed.
Retries after 1 min, 5 min, 30 min, 2 h and 12 h; after 5 attempts the delivery is abandoned, after 20 consecutive failures the address is disabled (re-enable it from the page). "Test" sends a HookTest.
The delivery body
{
"id": "d3b7...", // X-Vepra-Delivery
"event": "ContactSaved", // X-Vepra-Event
"occurredAt": "2026-09-17T18:40:00.000Z",
"business": "58a7...",
"aggregate": { "type": "Contact", "id": "a51d...", "version": 3 },
"payload": { "...": "..." },
"attempt": 1
}Verification (Node.js)
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verifyVepra(rawBody, signatureHeader, secret) {
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
const a = Buffer.from(signatureHeader ?? ''), b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}Verification (PHP)
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
$ok = hash_equals($expected, $_SERVER['HTTP_X_VEPRA_SIGNATURE'] ?? '');
The agent
The agent takes a goal in plain words and builds the calls itself: its tools are exactly the contract commands within the actor’s permissions. Invoices, payments, payroll, licences, periods and memberships are never executed by the agent; they stay proposals a person executes.
Scheduled runs execute a goal on a cadence (hourly, daily, weekdays, weekly) as their own actor, leave a task in Tasks and notify the creator. A tool-use model connected under API connections is required.
Workflows
The "Invoke command" step of a workflow also runs through the pipeline, as the business’s "Workflow" actor, with typed parameters (the contact tag, the booking action, the conversation channel…). Events that actor raises never start other workflows, so there are no endless chains. The WebhookReceived trigger connects outside systems to workflows.
Security
Tokens never in URLs; keys are stored hashed, secrets sealed. Every call is audited with the service actor that made it. Limits: up to 10 keys, 10 schedules, 20 inbound webhooks and 10 outbound addresses per business; inbound body up to 1 MB. The API rate limit applies to keys too.
Questions or a higher limit: kontakt@vepra.ai.