Authentication

The VerityPay API uses Sanctum personal access tokens scoped to a tenant. Each token carries a set of fine-grained abilities that gate which endpoints it can hit. There is no OAuth flow — bring-your-own-token from the dashboard.

Mint a token

  1. Sign in to your tenant dashboard.
  2. Open Settings → API Tokens → New Token.
  3. Give the token a descriptive name (e.g., billing-service-prod, integrations-staging). Future-you will thank present-you when an old token leaks and you need to know which workload it belongs to.
  4. Select the abilities the token needs (see Abilities below). Default to least-privilege.
  5. Copy the displayed token immediately — it is shown once and never again.

The token format looks like:

vp_pat_3|abc123...verylong...string

Treat it as a credential. Never check it in. Never paste it in a browser address bar. Always rotate when a developer leaves the team.

Send the token

Include the token on every request as a bearer credential:

GET /api/v1/contacts HTTP/1.1
Host: app.veritypay.com
Authorization: Bearer vp_pat_3|abc123...
Accept: application/json

The bearer token does two things at once:

  1. Resolves the tenant — the token is matched against the central token_routes table, which sets the schema for the request before authentication runs.
  2. Authenticates the caller — Sanctum then validates the token against that tenant's personal_access_tokens table.

You never send a tenant ID, slug, or schema header. The token is the tenant.

How abilities work

Abilities are slugs of the form api:{action}-{resource}. The controller calls $this->authorize($policyMethod, $resource), and the underlying gate accepts the call only when the token carries the matching ability.

The mapping is straightforward:

Policy method Token ability format Example
viewAny api:view-any-{resource} api:view-any-contact
view api:view-{resource} api:view-contact
create api:create-{resource} api:create-contact
update api:update-{resource} api:update-contact
delete api:delete-{resource} api:delete-contact
restore api:restore-{resource} api:restore-contact

Some lifecycle endpoints authorize against a non-CRUD policy method — for example, POST /payables/{id}/resend-notification calls authorize('resendNotification', $payable->transaction), which maps to the api:resend-transaction ability. The Endpoints reference lists the policy gate for every endpoint.

Abilities

The dashboard's New API Token form is the source of truth. As of the latest deploy, the assignable abilities are:

Contacts

api:view-any-contact
api:view-contact
api:create-contact
api:update-contact
api:delete-contact
api:restore-contact

Receivables

api:view-any-receivable
api:view-receivable
api:create-receivable
api:update-receivable
api:delete-receivable
api:restore-receivable

Payables

api:view-any-payable
api:view-payable
api:create-payable
api:update-payable
api:delete-payable
api:restore-payable

Transactions

api:view-any-transaction
api:view-transaction
api:view-token-transaction
api:create-transaction
api:update-transaction
api:delete-transaction
api:restore-transaction
api:resend-transaction

api:update-transaction is broad — it gates every payable/receivable lifecycle action that calls authorize('update', $transaction) (mark-paid, revoke-link, retry-payment, verify-payer, mark-disbursed, retry-disbursement, verify-identity, expire). Grant it deliberately.

Custom-field definitions

api:view-any-transaction-field-definition
api:view-transaction-field-definition
api:create-transaction-field-definition
api:update-transaction-field-definition
api:delete-transaction-field-definition
api:restore-transaction-field-definition

The contact-field-definition endpoints exist but their abilities are tenant-managed (not in the standard assignable set) — your tenant admin configures who can manage them.

Webhooks

api:view-any-webhook
api:view-webhook
api:create-webhook
api:update-webhook
api:delete-webhook

These gate the webhook-endpoint management endpoints. Note that api:update-webhook also covers rotate-secret — a token holding it can mint new signing secrets, so treat it as a credential-issuing grant.

Campaigns & messaging

api:view-any-campaign
api:view-campaign
api:create-campaign
api:update-campaign
api:delete-campaign
api:restore-campaign
api:assign-campaign

api:view-any-message
api:view-message
api:create-message
api:update-message
api:delete-message
api:restore-message
api:test-send-message

api:view-any-message-outbox
api:view-message-outbox
api:create-message-outbox
api:update-message-outbox
api:delete-message-outbox
api:restore-message-outbox
api:cancel-message-outbox

Opt-in & suppression

api:view-any-opt-in-code
api:view-opt-in-code
api:create-opt-in-code
api:update-opt-in-code
api:delete-opt-in-code
api:restore-opt-in-code
api:import-opt-in-code

api:view-any-suppression
api:create-suppression
api:delete-suppression

The opt-in endpoints are read-only and all three routes check api:view-any-opt-in-code; the remaining *-opt-in-code abilities exist for parity with the dashboard permissions and gate nothing over the API today. The suppression abilities gate the suppression list.

Other

api:view-payment-rail · api:update-payment-rail · api:delete-payment-rail
api:view-any-role · api:view-role · api:create-role · api:update-role · api:delete-role · api:restore-role
api:view-any-user · api:view-user · api:create-user · api:update-user · api:delete-user · api:restore-user
api:view-any-subscription · api:view-subscription · api:create-subscription · api:update-subscription · api:delete-subscription · api:restore-subscription
api:view-any-support-ticket · api:view-support-ticket · api:create-support-ticket · api:update-support-ticket · api:delete-support-ticket
api:view-any-tenant-config-meta · api:view-tenant-config-meta · api:create-tenant-config-meta · api:update-tenant-config-meta · api:delete-tenant-config-meta · api:restore-tenant-config-meta

The exact list available to your operator account depends on the permissions your dashboard role holds — the form intersects Permission::tenantAssignable() with the creator's own grants to prevent privilege escalation.

Bundle Abilities
Read-only reporting api:view-any-contact, api:view-contact, api:view-any-payable, api:view-payable, api:view-any-receivable, api:view-receivable, api:view-any-transaction, api:view-transaction
Send payouts (claims/refunds bot) api:create-payable, api:view-payable, api:delete-payable (for cancel), api:update-transaction (for revoke-link / retry / verify / expire), api:resend-transaction
Collect payments (invoice bot) api:create-receivable, api:view-receivable, api:delete-receivable (for cancel), api:update-transaction (for mark-paid / revoke-link / retry / verify / expire), api:resend-transaction
Contact sync (CRM mirror) api:create-contact, api:update-contact, api:view-any-contact, api:view-contact, api:delete-contact, api:restore-contact
Webhook self-management (ops bot) api:view-any-webhook, api:view-webhook, api:create-webhook, api:update-webhook (for rotate-secret / re-enable), api:delete-webhook

Token lifecycle best practices

  • One token per service, never per human. Rotate when any operator with access leaves.
  • Keep abilities minimal. A read-only reporting token cannot create payables — that's the whole point.
  • Rotate proactively. Quarterly is a reasonable default; immediately on any suspected leak.
  • Revoke unused tokens. The dashboard shows last-used timestamps; anything idle 90+ days should go.
  • Store in a secret manager. Never in environment files committed to repos. Never in client-side code.
  • Use separate tokens per environment. A staging token must not work in production — see the Sandbox → Production guide.

Authentication errors

Status error.code What it means
401 unauthenticated Token missing, malformed, expired, or revoked.
401 invalid_signature Signed-URL request had a bad/missing signature.
403 forbidden Token authenticated but lacks the required ability.

A 403 forbidden after a 200 from the same token usually means a new endpoint shipped that gates on an ability you didn't grant. Mint a new token (or update the existing one) and retry.