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-itemPOSTbody.options.on_duplicate—update(default),skip, orfail. Applies only to items that carry a caller-supplied external ID; items without one are always created.options.suppress_webhooks— defaulttrue: per-record*.created/*.updateddeliveries are withheld and replaced by onecontact.batch.completed/receivable.batch.completed/payable.batch.completedevent. Setfalseto restore per-record deliveries (the summary event is then not emitted).options.suppress_activity_log— defaulttrue: 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— defaultfalse(unlike the single-item endpoints, which always evaluate campaign rules). Settrueto 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 with422. 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 queued → processing → one of:
completed— every item succeeded.partial_failure— finished, but some items failed; pageGET …/errorsfor the per-row reasons.failed— the batch machinery itself failed (never used for rejected rows);failure_reasoncarries 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-codesaccepts?active=true|falseand returns the default code first, then newest first. Each code carries its publicurl(what the QR encodes),is_live,scan_countandopt_in_count.GET /opt-insaccepts?status=confirmed|pending|revoked,?opt_in_code_id=,?contact_id=and?since=<ISO-8601>; results are newestconsented_atfirst. A double opt-in consent stayspendinguntil the contact replies YES to the confirmation text.GET /contacts/{contact}additionally exposesopt_in_status(none,pending,opted_in,opted_out) and the contact'sconsents.
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). Plainhttp://is rejected with422.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 with422. The full catalog is in the Webhooks guide.is_active— optional boolean, defaults totrueon 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-gatewayfeature flag. When the flag is off, every gateway route returns403. 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.