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 + identify
  • server — full read + write; never embed in a client app
  • admin — 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 profile
  • GET /v1/profiles/{id} — fetch by id or external id
  • POST /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 list
  • POST /v1/sends — send a transactional or marketing email
  • GET /v1/sends/{id} — fetch a send + timestamp ladder
  • GET /v1/sends/list — cursor-paginated list
  • POST /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/events returns { id, idempotent: true }— the original event's id, with a boolean flag.
  • /v1/sends returns { ok: true, sendId, status: 'idempotent' } — the original send's id, with a string status. There is no boolean idempotent field 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 — replayed idempotencyKey; the original sendId is 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; no sendId is returned (ok: false).
  • no_email — the profile has no email address on file (ok: false).
  • failed — a provider/send error; see reason (ok: false).
  • over_quota — the workspace's monthly send cap is reached; see quota (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.