Authentication
Every request requires a tenant-scoped API key. Generate one at /app/settings/api-keys after signing in. Keys are tenant-bound, write-revocable, and rate-limited per-key.
Include the key on every call via either header:
X-Api-Key: vb_live_a1b2c3d4... # or Authorization: Bearer vb_live_a1b2c3d4...
Keys are shown ONCE on creation. Store them in your secret manager and never commit them. Compromised keys can be revoked from the same settings page; new requests with the revoked key 401 within seconds.
Rate limits
Per-key sliding-window limits, enforced at the edge. Defaults:
Over-limit responses return 429 with these headers:
Retry-After: 12 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 2026-04-29T13:00:00Z
Need higher limits? Email support@hellovelora.com with your traffic profile — we'll raise individual buckets per tenant rather than charge for an arbitrary “Pro” tier.
Error format
All errors return JSON with a single error field. Validation failures additionally include a issues array (Zod-shaped — same as the request body).
{
"error": "Invalid body",
"issues": [
{ "code": "invalid_type", "path": ["amountClaimed"], "message": "Expected number" }
]
}Pagination
List endpoints use cursor pagination. Pass ?cursor=<id>&limit=50; the response includes nextCursor when more rows exist (null at end). Default limit varies per endpoint, max is 200.
GET /api/webhooks/wh_abc/deliveries?limit=50&cursor=del_xyz
{
"deliveries": [...],
"nextCursor": "del_qrs"
}Endpoints
The integration-friendly subset. The full app surface is larger but these are stable and documented.
GET /api/integrations/recon-summary
Cross-product summary for embedding a Velora tile in your dashboard. Returns counts of unresolved discrepancies, urgent (HIGH/CRITICAL) findings, open disputes, and the timestamps of the latest invoice + recon run.
{
"pendingTickets": 7,
"urgentTickets": 2,
"openDisputes": 4,
"lastInvoiceAt": "2026-04-28T14:00:00Z",
"lastReconAt": "2026-04-28T14:01:23Z",
"asOf": "2026-04-29T20:00:00Z"
}POST /api/recon
Trigger a reconciliation run on a previously-uploaded invoice. Idempotent on (invoiceId, runKey) — re-posting the same key returns the original run id.
GET /api/audits/[runId]/csv
Discrepancies for a run as RFC 4180 CSV. Auth-logged. ?format=qbo|netsuite on the GL endpoint variant returns balanced journal entries directly importable into QuickBooks Online or NetSuite.
GET /api/webhooks/[id]/deliveries
The append-only delivery log. Filter ?status=ok|failed, paginate with cursor. Each row includes status, statusCode, duration, errorMessage, and a retryable boolean — true when we still have the original payload retained for re-fire.
POST /api/webhooks/deliveries/[id]/retry
Re-fire a previous delivery using the EXACT original payload + signature. The new attempt creates a new delivery row pointing at the original via retryOfId; the original is never mutated. Receivers depending on idempotency keys correctly treat it as a duplicate.
Webhooks
Outbound, signed, retried with exponential backoff (0s, 5s, 30s, 5m, 30m). Subscribe at /app/settings/webhooks; pick the events you want. Each subscription has its own signing secret — rotate by deleting and re-creating.
Every delivery sends:
POST https://your-receiver.example.com/velora Content-Type: application/json X-Webhook-Event: dispute.sla_breached X-Webhook-Delivery: del_abc123 X-Webhook-Timestamp: 1714400000 X-Webhook-Signature: sha256=... User-Agent: Velora-Billing-Webhook/1.0
Event catalog
Fourteen events fire today. Subscriptions can include any subset.
Need an event we don't emit yet? Open an issue on GitHub. Adding a new event is two lines of dispatch + a doc update.
Verifying signatures
Compute HMAC-SHA256 of the raw request body using the subscription's signing secret, prefix with sha256=, and compare to X-Webhook-Signature. Use a constant-time compare to avoid timing oracles.
// Node 20+
import { createHmac, timingSafeEqual } from "crypto";
function verify(body: string, header: string, secret: string): boolean {
const expected = "sha256=" + createHmac("sha256", secret)
.update(body, "utf8").digest("hex");
if (header.length !== expected.length) return false;
return timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}
// In your handler:
const body = await req.text(); // RAW body, not the parsed object
if (!verify(body, req.headers.get("x-webhook-signature") ?? "",
process.env.VELORA_WEBHOOK_SECRET!)) {
return new Response("invalid signature", { status: 401 });
}The X-Webhook-Timestamp header is the dispatch time in epoch seconds. Reject requests older than 5 minutes to prevent replay.
Slack & Microsoft Teams
Paste a Slack incoming-webhook URL (hooks.slack.com/services/...) or Teams connector URL (*.webhook.office.com/...) into the standard webhook form. We detect the channel and translate each event into the channel's native rich-card schema:
- Slack: Block Kit message — header + section with up to 4 fact pairs + an action button linking back into the app.
- Teams: MessageCard schema with brand themeColor, fact table, and OpenUri action.
Chat-channel deliveries are unsigned — Slack and Teams treat URL secrecy as the auth credential and don't define a verify scheme on incoming webhooks. Delivery rows still capture status + statusCode + duration the same way as signed deliveries.
Manual retry
The deliveries log retains the original payload (up to 16 KiB). Failed deliveries can be re-fired manually from the UI or via API. The retry uses the EXACT original payload + signature, so receivers depending on idempotency keys treat it correctly as a duplicate.
POST /api/webhooks/deliveries/del_abc123/retry
# Response
{
"deliveryId": "del_xyz789",
"status": "ok",
"statusCode": 200
}Old deliveries (pre-2026-04-28) and over-cap payloads return 409 with "error": "Original payload was not retained".