API reference
The PAD HTTP API is documented as OpenAPI 3.1 at /v1/openapi.json. Point Swagger UI, Postman, or your favorite OpenAPI codegen at it.
Authentication
Every request includes Authorization: Bearer <api_key>. Create keys at /dashboard/settings/api-keys; pick the right scope:
browser— safe to ship to the client; limited to event tracking + identifyserver— full read + write; never embed in a client appadmin— workspace admin operations (rare)
Quick start (curl)
# Identify a profile
curl -X POST https://pad.thevisualpay.com/v1/profiles \
-H "Authorization: Bearer pad_sk_..." \
-H "Content-Type: application/json" \
-d '{"externalId":"u_42","email":"ada@acme.com","traits":{"plan":"pro"}}'
# Track an event (triggers campaigns)
curl -X POST https://pad.thevisualpay.com/v1/events \
-H "Authorization: Bearer pad_sk_..." \
-H "Content-Type: application/json" \
-d '{"event":"checkout.completed","externalId":"u_42","properties":{"amount":99}}'
# Send a transactional email
curl -X POST https://pad.thevisualpay.com/v1/sends \
-H "Authorization: Bearer pad_sk_..." \
-H "Content-Type: application/json" \
-d '{"template":"welcome","to":"u_42","variables":{"firstName":"Ada"}}'
# Poll send status
curl https://pad.thevisualpay.com/v1/sends/<send_id> \
-H "Authorization: Bearer pad_sk_..."Endpoints
POST /v1/profiles— upsert a profileGET /v1/profiles/{id}— fetch by id or external idPOST /v1/events— track a single event (triggers campaigns)POST /v1/events/bulk— ingest up to 500 events (no campaign enqueue)GET /v1/events/list— cursor-paginated listPOST /v1/sends— send a transactional or marketing emailGET /v1/sends/{id}— fetch a send + timestamp ladderGET /v1/sends/list— cursor-paginated listPOST /v1/consents— record consent for (profile, channel, category)
Pagination
List endpoints return { data: [...], nextCursor }. Pass nextCursor back in the next request to fetch the next page. nextCursor: null means end-of-list.
Idempotency
Both /v1/events and /v1/sends accept an idempotencyKey, but the two endpoints don't reply the same way when a key is replayed:
/v1/eventsreturns{ id, idempotent: true }— the original event's id, with a boolean flag./v1/sendsreturns{ ok: true, sendId, status: 'idempotent' }— the original send's id, with a string status. There is no booleanidempotentfield on this endpoint.
Retention:a key is deduplicated for the lifetime of the underlying record — currently governed by a ~365-day data retention window, not a short TTL. Don't reuse a key expecting it to "expire" soon.
Scope: keys are deduplicated per workspace only — not automatically combined with recipient or template. Reusing the same key for a different recipient or template on the same workspace is treated as a duplicate of the first call and silently short-circuited (nothing new is sent/tracked). Bake the recipient/template identity into the key yourself, e.g. send-42-invoice_paid-2026-05-20 rather than a plain date-only key like checkout_2026-05-20.
POST /v1/sends response status
Every response is a 200 — a refused send is a decision, not a transport error. Check ok and status:
sent— handed to the provider (ok: true).idempotent— replayedidempotencyKey; the originalsendIdis returned and nothing new was sent (ok: true). See Idempotency above.suppressed— recipient is on the suppression list (ok: false).no_consent— the template's category requires consent and none is on file for this profile; nosendIdis returned (ok: false).no_email— the profile has no email address on file (ok: false).failed— a provider/send error; seereason(ok: false).over_quota— the workspace's monthly send cap is reached; seequota(ok: false).
Everything except sent and failed is a decision made before any provider call — retrying idempotent/suppressed/no_consent/no_email/over_quotawon't change the outcome.
over_quota specifics:not retryable within the window — the cap resets at the next UTC calendar-month boundary (00:00 UTC on the 1st), or immediately on a plan upgrade. It's scoped per workspace against your organization's plan tier — each workspace in an org carries its own counter against the same tier limit, not scoped per sending domain. If a retry can't succeed until the reset, stop retrying and surface it to an operator instead — polling won't clear it early.
Errors
All errors are JSON with shape { error: { message, code? } }. 401 = bad auth. 400 = bad request body. 404 = resource not in this workspace. 5xx = retry with exponential backoff.
Webhooks
PAD can also POST events to you — see webhooks for the signed outbound delivery contract.