Vortex

API AND WEBHOOKS

Vortex API and webhooks

Read your prospects, clients and analytics from your own tools, and get a signed call when something happens in your workspace.

Included in the Agency plan. Read-only: the API cannot create, change or delete anything.

Authentication

Create a key in Settings, API and webhooks. It looks like vtx_ followed by 8 characters, an underscore and 32 characters. It is shown once: Vortex only stores a hash of it, so keep it in your password manager. Send it in the Authorization header.

Example
curl https://usevortex.tech/api/v1/prospects \
  -H "Authorization: Bearer vtx_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
  • A key belongs to one workspace and can only read that workspace.
  • A revoked key stops working at once. Revoking works on any plan.
  • Keys have scopes. The API accepts keys with the api:read scope.
  • If the workspace leaves the Agency plan or its subscription stops, every request is refused with 403.

Limits

  • 60 requests per minute per key, in fixed one-minute windows. Over the limit you get 429 with a Retry-After header in seconds. Every successful response carries X-RateLimit-Limit.
  • Lists return 25 items by default and at most 100 (parameter limit).
  • Up to 10 active keys and 5 webhook endpoints per workspace.

Responses and errors

Success: { "data": ... }. Lists add "next_cursor" (null on the last page). Errors: { "error": { "code": "...", "message": "..." } }.

StatusCodeMeaning
400invalid_limit, invalid_cursor, invalid_status, invalid_daysA parameter is not valid
401invalid_keyMissing, malformed, unknown or revoked key (always the same answer)
403plan_required, subscription_inactive, insufficient_scope, feature_unavailableThe key is valid but the workspace may not use it
404not_foundNo such object in your workspace
429rate_limitedToo many requests

Endpoints

All endpoints use GET and start with https://usevortex.tech/api/v1.

EndpointParametersReturns
GET /api/v1-Checks the key and describes the API
GET /api/v1/prospectsstatus, limit, cursorProspects, newest first
GET /api/v1/prospects/{id}-One prospect
GET /api/v1/clientslimit, cursorClients: id, prospect_id, website_url
GET /api/v1/clients/{id}/analyticsdays (1-90)Aggregated portal numbers already collected
GET /api/v1/paymentsstatus, limit, cursorClient payments collected through Stripe Connect

Prospect status values: new, active, standby, client, blacklisted. Payment status values: paid, pending, expired, void, uncollectible, open. Pass next_cursor back as cursor to get the next page.

A prospect
{
  "data": [
    { "id": "…", "status": "new", "city": "Lyon", "niche": "Bakery", "siren": "123456789", "created_at": "2026-10-01T10:00:00.000Z" }
  ],
  "next_cursor": "eyJhdCI6…"
}
Client analytics
{ "data": { "client_id": "…", "period": { "days": 30, "from": "…", "to": "…" },
  "visitors": 120, "pageviews": 340, "form_submits": 6, "call_clicks": 11,
  "conversion_rate_pct": 5, "avg_load_time_ms": 900, "truncated": false } }

Payments are what your clients paid you through Vortex (amount_cents, application_fee_cents, currency, status, created_at). Example prospects shown to new accounts are not returned.

Fields that are never returned

Content that comes from Google Places is not exposed by the API or the webhooks: name, address, phone, hours, rating, reviews, google_maps_url. The business name of a client is not exposed either. E-mail addresses, portal links and invoice links are never returned.

Webhooks

Add an endpoint in Settings, API and webhooks, and choose its events. Vortex then sends a POST with a JSON body when the event happens. The signing secret (whsec_…) is shown once when you add the endpoint.

  • https:// only, the default port, and a real domain name (no IP address, no localhost, no internal name).
  • The name must resolve to public addresses. This is checked again at every delivery, and the connection goes to the address that was checked.
  • Redirects are not followed.

Events and payload

EventWhen
prospect.createdA prospect is added, by hand or from a search
prospect.status_changedA prospect moves to another pipeline stage (previous_status is included)
payment.receivedA client payment is settled through Stripe Connect
webhook.testOnly when you press "Send a test"
Payload
{
  "id": "6f1c1d5e-6b0e-4d0b-9a3a-0e5a1b2c3d4e",
  "type": "prospect.created",
  "created": "2026-10-23T10:00:00.000Z",
  "data": { "id": "…", "status": "new", "city": "Lyon", "niche": "Bakery", "siren": null, "created_at": "…" }
}

id is the event id: the same event is never delivered twice to one endpoint, and the id stays the same across retries, so you can use it to deduplicate.

HeaderValue
Vortex-Event-IdThe event id
Vortex-Event-TypeThe event type
Vortex-Delivery-Attempt1 for the first try
Vortex-Signaturet=<unix seconds>,v1=<hex>

Verifying the signature

v1 is HMAC-SHA256(secret, "<t>.<raw request body>") in hexadecimal. Compute it over the raw body, compare in constant time and reject timestamps older than 5 minutes.

Node.js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret, header, rawBody, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const given = Buffer.from(parts.v1 ?? "");
  const wanted = Buffer.from(expected);
  return given.length === wanted.length && timingSafeEqual(given, wanted);
}
Python
import hashlib, hmac, time

def verify(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t = int(parts["t"])
    if abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(parts.get("v1", ""), expected)

Delivery, retries and failures

  • Answer with any 2xx within 10 seconds. Anything else, including a redirect, is a failure.
  • A failed delivery is retried after 1 min, 5 min, 30 min, 2 h, 6 h (6 attempts in total). The schedule is stored in the database, so it survives restarts.
  • After 10 failed attempts in a row on one endpoint, the endpoint is switched off and shows the reason in Settings. Switching it back on resets the counter.
  • The delivery log in Settings keeps the last 30 days: date, event, result, attempts and the HTTP status or a short error code. The response of your server is never stored.
Vortex API and webhooks — Vortex