Endpoints

Reference for every tenant-facing API endpoint. All routes are prefixed with /api/v1 and require the authentication conventions — bearer token + the listed policy gate — unless otherwise noted.

Conventions

  • {contact}, {payable}, {receivable}, {transaction}, {webhook_endpoint}, {batch}, {optInCode} are tenant-scoped IDs (ULID).
  • {contactPhone} is a URL-encoded E.164 phone number.
  • All write endpoints accept Idempotency-Key.
  • All endpoints emit X-Request-Id.
  • Paginated collection endpoints accept ?per_page= (capped at 100; defaults vary by resource, 15–25).
  • The Policy gate column lists what the controller calls via $this->authorize(...). The token ability slug it maps to is shown on the New API Token form in your dashboard — the canonical list lives in Authentication → Abilities.

Health

GET /api/v1/healthcheck/app

Lightweight liveness probe. Returns 200 { "status": "ok" } when the application is up. Does not require authentication; safe to call from load balancers.


Contacts

The recipient/sender directory. Every transaction references one contact.

Method Path Policy gate
GET /contacts viewAny on Contact
POST /contacts create on Contact
GET /contacts/{contact} view on the contact
PUT /contacts/{contact} update on the contact
PATCH /contacts/{contact} update on the contact
DELETE /contacts/{contact} delete on the contact
POST /contacts/{contact}/restore restore on the contact

DELETE /contacts/{contact} is a soft delete; the contact remains queryable with ?with_trashed=1 and can be restored.

Bulk-onboarding a contact book? Use POST /contacts/batch — see Batch ingest below.


Contact field definitions

Custom-field schema for contacts (per tenant). Tenant-managed; abilities are configured by your tenant administrator.

Method Path Policy gate
GET /contact-field-definitions viewAny on ContactFieldDefinition
POST /contact-field-definitions create on ContactFieldDefinition
GET /contact-field-definitions/{definition} view on the definition
PUT /contact-field-definitions/{definition} update on the definition
DELETE /contact-field-definitions/{definition} delete on the definition

Transaction field definitions

Custom-field schema for transactions (per tenant).

Method Path Policy gate
GET /transaction-field-definitions viewAny on TransactionFieldDefinition
POST /transaction-field-definitions create on TransactionFieldDefinition
GET /transaction-field-definitions/{definition} view on the definition
PUT /transaction-field-definitions/{definition} update on the definition
DELETE /transaction-field-definitions/{definition} delete on the definition

The api:view-any-transaction-field-definition, api:create-transaction-field-definition, etc. abilities are tenant-assignable.


Receivables

Money you collect from a customer (one-tap SMS payment links).

CRUD

Method Path Policy gate
GET /receivables viewAny on Receivable
POST /receivables create on Receivable
GET /receivables/{receivable} view on the receivable
PUT /receivables/{receivable} update on the receivable
DELETE /receivables/{receivable} delete on the receivable

Lifecycle actions

All lifecycle actions authorize via update on the underlying transaction (token ability api:update-transaction) except cancel, which uses delete, and resend-notification, which uses resendNotification (api:resend-transaction).

Method Path Policy gate
POST /receivables/{receivable}/cancel delete on the receivable
POST /receivables/{receivable}/resend-notification resendNotification on the parent transaction
POST /receivables/{receivable}/mark-paid update on the parent transaction
POST /receivables/{receivable}/revoke-link update on the parent transaction
POST /receivables/{receivable}/retry-payment update on the parent transaction
POST /receivables/{receivable}/verify-payer update on the parent transaction
POST /receivables/{receivable}/expire update on the parent transaction

cancel is rejected with 409 once the customer has paid. resend-notification re-issues the SMS payment link to the same contact. revoke-link invalidates the existing payment link without cancelling the receivable. retry-payment re-attempts the underlying processor call after a transient failure. mark-paid records an out-of-band payment. expire ages out the link.

Importing historical or bulk receivables? Use POST /receivables/batch — see Batch ingest below.


Payables

Money you pay to a customer (claims, refunds, settlements, commissions).

CRUD

Method Path Policy gate
GET /payables viewAny on Payable
POST /payables create on Payable
GET /payables/{payable} view on the payable
PUT /payables/{payable} update on the payable
DELETE /payables/{payable} delete on the payable

Lifecycle actions

Method Path Policy gate
POST /payables/{payable}/cancel delete on the payable
POST /payables/{payable}/resend-notification resendNotification on the parent transaction
POST /payables/{payable}/mark-disbursed update on the parent transaction
POST /payables/{payable}/revoke-link update on the parent transaction
POST /payables/{payable}/retry-disbursement update on the parent transaction
POST /payables/{payable}/verify-identity update on the parent transaction
POST /payables/{payable}/expire update on the parent transaction

cancel is allowed only while the recipient has not yet selected a destination. resend-notification re-delivers the destination-selection SMS. mark-disbursed records an out-of-band disbursement. retry-disbursement re-attempts the underlying processor call. verify-identity runs the identity check against the recipient's selected destination. revoke-link invalidates the existing destination-selection link. expire ages out the link.

State transitions are enforced by the underlying state machine — invalid transitions return 422 invalid_transition with the offending state pair in error.message.

Importing historical or bulk payables? Use POST /payables/batch — see Batch ingest below.


Transactions

Cross-cutting actions that apply to either a payable or a receivable.

Method Path Policy gate
POST /transactions/{transaction}/campaign-assignment api:assign-campaign

Assigns the transaction to a campaign for grouped reporting and follow-up notifications. Returns 409 if the transaction is already assigned unless reassign: true is sent; the campaign must be active, published, and match the transaction's direction.


Batch ingest

Asynchronous bulk writes for contacts, receivables, and payables — built for onboarding an existing contact book or importing historical transactions without making thousands of sequential single-item calls. Submission is per resource; reporting is shared.

Submitting a batch requires no new ability: each submit endpoint authorizes exactly what the equivalent single-item write does (api:create-contact, api:create-receivable, api:create-payable), and the read surface uses the matching view-any abilities.

Submit

Method Path Policy gate
POST /contacts/batch create on Contact
POST /receivables/batch create on Receivable
POST /payables/batch create on Payable

A submission returns 202 Accepted immediately with the batch header — nothing is written during the request. Rows are validated and applied asynchronously; poll the batch (or subscribe to the *.batch.completed webhook) for the outcome.

Read

Method Path Policy gate
GET /batches viewAny on the batch's underlying model
GET /batches/{batch} viewAny on the batch's underlying model
GET /batches/{batch}/errors viewAny on the batch's underlying model

GET /batches accepts ?type=contacts|receivables|payables, ?status=, and ?per_page= (max 100). Without a type filter, the list is narrowed to the types your token may read. GET …/errors returns per-row failures, paginated in row_index order: { row_index, external_id, field, message }.

Request envelope

{
  "items": [ { /* same shape as the single-item POST body */ }, … ],
  "options": {
    "on_duplicate": "update",
    "suppress_webhooks": true,
    "suppress_activity_log": true
  }
}
  • items — required, 1 to 10,000 items for contacts, 1 to 5,000 for receivables/payables. Each item takes the same fields as the corresponding single-item POST body.
  • options.on_duplicateupdate (default), skip, or fail. Applies only to items that carry a caller-supplied external ID; items without one are always created.
  • options.suppress_webhooks — default true: per-record *.created/*.updated deliveries are withheld and replaced by one contact.batch.completed / receivable.batch.completed / payable.batch.completed event. Set false to restore per-record deliveries (the summary event is then not emitted).
  • options.suppress_activity_log — default true: one summary activity entry per batch instead of one per record.

Receivable/payable batches accept three additional options:

  • options.campaign_id — assign every item directly to this campaign (no per-row rule evaluation).
  • options.auto_assign_campaign — default false (unlike the single-item endpoints, which always evaluate campaign rules). Set true to run campaign auto-assignment for each imported transaction on a background bulk queue.
  • options.acknowledge_messaging_volume — required (true) when the projected outbound messaging volume (items × campaign sequence steps) exceeds 10,000 messages; otherwise the submission is rejected with 422. This is a deliberate guard: campaign assignment queues real, billable SMS.

Only the envelope is validated synchronously — a malformed envelope is a 422 and no batch is created. Individual items are validated inside the background job against the same rules as the single-item endpoint, so one bad row becomes a row error rather than rejecting the other rows.

Contact matching

Receivable/payable items find-or-create their contact by external contact ID: an unmatched ID creates a contact from the item's name fields (unlike the single-item endpoints, which 422 on an unknown contact reference). Supply stable external IDs if you need resubmission to converge on the same records — items without one are always created, and on_duplicate does not apply to them.

Batch lifecycle

The batch header returned on submit and by GET /batches/{batch}:

{
  "data": {
    "id": "01JZK…",
    "type": "contacts",
    "status": "queued",
    "total_items": 8000,
    "processed_items": 0,
    "succeeded_items": 0,
    "failed_items": 0,
    "options": { "on_duplicate": "update", "suppress_webhooks": true, "suppress_activity_log": true },
    "failure_reason": null,
    "started_at": null,
    "completed_at": null,
    "created_at": "2026-08-09T14:00:00+00:00",
    "updated_at": "2026-08-09T14:00:00+00:00",
    "links": { "self": "…/batches/01JZK…", "errors": "…/batches/01JZK…/errors" }
  }
}

status progresses queuedprocessing → one of:

  • completed — every item succeeded.
  • partial_failure — finished, but some items failed; page GET …/errors for the per-row reasons.
  • failed — the batch machinery itself failed (never used for rejected rows); failure_reason carries a sanitized explanation.

Batch limits

Limit Value On breach
Items per contacts batch 10,000 422 validation_failed
Items per receivables/payables batch 5,000 422 validation_failed
Request payload size 5 MB 413 batch_payload_too_large
Batch submissions 10 / minute / tenant (default) 429 rate_limited
Concurrent batches (queued + processing) 2 per tenant (default) 429 batch_concurrency_limit_reached + Retry-After

The payload cap is usually the binding one for rich rows — a batch of maximally-populated contacts hits 5 MB well before 10,000 items. Split large imports into multiple sequential batches. Both submission caps are per tenant, not per token, and are platform-configurable per tenant — if a large migration needs more headroom, ask your VerityPay contact.

Whole-request retries are covered by Idempotency-Key like every other write. For row-level idempotency across batches, supply external IDs and rely on on_duplicate.


Opt-in (read-only)

SMS consent captured through QR codes and public opt-in links. Codes and consents are created in the dashboard (and by the inbound-SMS YES reply); the API only reads them. Every route in this group is gated by the tenant's opt-in capability and returns 404 — not 403 — when the capability is off.

Method Path Policy gate
GET /opt-in-codes viewAny on OptInCode (api:view-any-opt-in-code)
GET /opt-in-codes/{optInCode} viewAny on OptInCode (api:view-any-opt-in-code)
GET /opt-ins viewAny on OptInCode (api:view-any-opt-in-code)
  • GET /opt-in-codes accepts ?active=true|false and returns the default code first, then newest first. Each code carries its public url (what the QR encodes), is_live, scan_count and opt_in_count.
  • GET /opt-ins accepts ?status=confirmed|pending|revoked, ?opt_in_code_id=, ?contact_id= and ?since=<ISO-8601>; results are newest consented_at first. A double opt-in consent stays pending until the contact replies YES to the confirmation text.
  • GET /contacts/{contact} additionally exposes opt_in_status (none, pending, opted_in, opted_out) and the contact's consents.

Suppression list

Phone numbers that must never receive transactional SMS — opt-outs, do-not-contact, regulatory blocks.

Method Path Policy gate
GET /suppression-list?channel=sms viewAny on SuppressedPhone (api:view-any-suppression)
POST /suppression-list create on SuppressedPhone (api:create-suppression)
DELETE /suppression-list/{contactPhone} delete on SuppressedPhone (api:delete-suppression)

POST /suppression-list accepts { contact_phone_id, reason: 'manual'|'bounce'|'complaint', channel: 'sms'|'email' }. The list endpoint returns the last-four phone digits and supports ?channel= filtering plus ?per_page= pagination (max 100). An unrecognized channel value on the list or delete endpoint returns a 422 validation error rather than defaulting to sms.


Webhook endpoints

Self-service management of outbound webhooks — register a delivery URL, choose which events it receives, rotate its signing secret, and inspect delivery attempts. See the Webhooks guide for the delivery contract (envelope, signature verification, retries).

CRUD

Method Path Policy gate
GET /webhook-endpoints viewAny on WebhookEndpoint
POST /webhook-endpoints create on WebhookEndpoint
GET /webhook-endpoints/{webhook_endpoint} view on the endpoint
PUT/PATCH /webhook-endpoints/{webhook_endpoint} update on the endpoint
DELETE /webhook-endpoints/{webhook_endpoint} delete on the endpoint

Actions

Method Path Policy gate
POST /webhook-endpoints/{webhook_endpoint}/rotate-secret update on the endpoint
GET /webhook-endpoints/{webhook_endpoint}/deliveries view on the endpoint

Request body (create / update)

{
  "name": "erp-mirror-prod",
  "url": "https://erp.example.com/hooks/veritypay",
  "subscribed_events": ["transaction.*", "contact.created"],
  "is_active": true
}
  • name — required, ≤120 chars.
  • url — required, HTTPS only (≤2048 chars). Plain http:// is rejected with 422.
  • subscribed_events — required, at least one entry. Each entry is an exact event type, a single-segment domain wildcard (payable.*), or the catch-all *. Unknown event types are rejected with 422. The full catalog is in the Webhooks guide.
  • is_active — optional boolean, defaults to true on create.

Signing secret (reveal-once)

The signing_secret is generated server-side — you cannot supply it. It appears in plaintext only in the 201 create response and the rotate-secret response; index, show, and update never include it. Store it on receipt. If it leaks, POST …/rotate-secret replaces it and returns the new plaintext once — subsequent deliveries sign with the new secret, and delivery history is preserved.

Delivery history

GET …/deliveries returns the endpoint's delivery attempts, paginated newest-first. Each row exposes event_type, event_id, status, attempt_count, response_status, last_error, next_attempt_at, and delivered_at — enough to debug missed events without operator involvement.

Endpoint responses also carry health fields: consecutive_failures, last_succeeded_at, last_failed_at, and disabled_at (set when the platform auto-disables a persistently failing endpoint — see the Webhooks guide).


AI gateway

A REST mirror of the VerityPay MCP gateway tools, for AI clients that consume an OpenAPI action schema rather than MCP. The flow is resolve → prepare → confirm → status: resolve (or create) the contact, prepare a draft transaction (nothing is sent), then confirm it to send or schedule.

Feature-gated. The whole surface sits behind the per-tenant ai-gateway feature flag. When the flag is off, every gateway route returns 403. Ask your VerityPay contact to enable it.

Method Path Purpose Policy gate
POST /gateway/resolve-contact Resolve a recipient by name (optionally narrowed by phone/email) into matched / ambiguous / not_found candidates. viewAny on Contact
POST /gateway/contacts Create a contact when resolution returned no match. Returns 201. create on Contact
POST /gateway/transactions/prepare Phase one: preview a payment and create a Draft (nothing is sent). Returns 201. create on Payable / Receivable by direction
POST /gateway/transactions/{transaction}/confirm Phase two: confirm and send/schedule the prepared transaction. create on Payable / Receivable by direction
GET /gateway/transactions/{reference}/status Read back a transaction's status and its latest message's dispatch state. view on the Payable / Receivable

Request/response schemas for the gateway are auto-generated (Scramble) and published at /docs/api.json on your tenant host — point OpenAPI-consuming agents there rather than hand-coding shapes.


Quick-reference matrix

Resource List Read Create Update Delete Lifecycle actions
Contacts restore, batch
Contact field definitions
Transaction field defs
Receivables cancel, resend-notification, mark-paid, revoke-link, retry-payment, verify-payer, expire, batch
Payables cancel, resend-notification, mark-disbursed, revoke-link, retry-disbursement, verify-identity, expire, batch
Transactions campaign-assignment
Batches errors (submit via each resource's /batch endpoint)
Opt-in codes / opt-ins read-only; 404 without the opt-in capability
Suppression list
Webhook endpoints rotate-secret, deliveries
AI gateway resolve-contact, contacts, transactions/prepare, transactions/confirm, transactions/status

For copy-pasteable example requests, see the Postman collection.