X-Private-Key header.Welcome to the Expi API documentation. All endpoints are accessed via the central query handler.
The API is built for quick integration: send JSON requests, receive JSON responses, and keep all payment workflows in one place. Use it to build custom checkout experiences, manage customers, issue invoices, and automate recurring billing with minimal overhead.
If you are new to the platform, start with Authentication, then explore the endpoint sections for the resources you need. Each section highlights core fields and common workflows.
https://your-domain.com/query
Every request must be authenticated. You can do this via Basic Auth or by including credentials in the JSON body. Pick one method and keep it consistent across your integration.
Use your merchant username as x_login and your secret key as x_tran_key for protected merchant endpoints. The login token flow on /query/auth/* is public and uses user credentials instead.
POST /query/auth/mfatoken — Validates username/password and returns an mfa_token.POST /query/auth/token — If payload contains mfa_token and code, verifies MFA and issues an access token.POST /query/auth/token — If payload contains username (or email) and password, it also requires trusted-device credentials to issue an access token.GET /query/me — Returns the current authenticated user, merchant, and organization context (requires authentication).{
"username": "user@example.com",
"password": "your_password"
}
{
"result": "success",
"mfa_token": "dummy_mfa_token",
"expires_in": 300
}
{
"mfa_token": "dummy_mfa_token",
"code": "123456",
"remember_device": 1
}
{
"username": "user@example.com",
"password": "your_password",
"trusted_device_id": "32_hex_device_id",
"trusted_device_token": "device_token"
}
{
"result": "success",
"user_id": 12,
"merchant": "MERCHANTCODE",
"access_token": "dummy_access_token",
"token_type": "Bearer",
"expires_in": 3600,
"trusted_device_id": "32_hex_device_id",
"trusted_device_token": "device_token",
"trusted_device_expires_in": 2592000
}
Authorization: Bearer <access_token>
Authorization: Basic <base64(username:secret_key)>
Base64 must be generated from the exact string username:secret_key (one colon, no extra spaces).
{
"x_login": "your_username",
"x_tran_key": "your_secret_key"
}
result: "success" with mfa_token and expires_in.remember_device is omitted during MFA verification, the device is remembered by default.trusted_device_id and trusted_device_token; otherwise it fails with Unknown Device.Streamline Transactions with Expitrans API
Our REST API allows developers to integrate online payment functionalities into their applications. By making API requests, you can process transactions, manage customers, handle subscriptions, and generate invoices programmatically.
The API uses JSON format for requests and responses, ensuring seamless communication between your application and our payment gateway. Authentication is required for secure access, and each request must include the necessary credentials.
Our REST API provides several key functionalities:
List endpoints support optional pagination query parameters:
page (optional): Page number to return (1-based).pageSize (optional): Number of records per page. Defaults to 50 when not provided.Pagination is only applied when page is provided. If page is omitted, the full list is returned.
These endpoints allow businesses to automate payment workflows and enhance their integration capabilities. In the next sections, we’ll provide detailed instructions on how to use each API.
Create short-lived card tokens for use with the Charges API.
Authentication:
- Authorization: Bearer <access_token> (recommended if you use the login/JWT flow)
- or Authorization: Basic (merchant x_login / x_tran_key)
- or include x_login and x_tran_key in the JSON body
| Field | Type | Required | Description |
|---|---|---|---|
number | string | Yes | 16-digit card number. |
month | string | Yes | 2-digit month 01–12. |
year | string | Yes | 2-digit year YY. |
x_login | string | No | Merchant login (only if not using Authorization header). |
x_tran_key | string | No | Merchant tran key/secret (only if not using Authorization header). |
{
"number": "4242424242424242",
"month": "12",
"year": "30"
}
{
"result": "success",
"token": "tok_..."
}
429 with Retry-After.Charge operations backed by Transactions. A charge id corresponds to the Transaction presentation_id within the authenticated merchant scope.
Authentication:
- Authorization: Bearer <access_token> (recommended if you use the login/JWT flow)
- or Authorization: Basic (merchant x_login / x_tran_key)
- or include x_login and x_tran_key in the JSON body
Idempotency (optional):
Send an Idempotency-Key header (or idempotency_key body field, max 128 characters) on POST /charges and POST /charges/{id}/capture to safely retry requests without charging the card twice. If a request with the same key already completed, the original response is replayed with an Idempotency-Replayed: true header. Reusing a key with a different payload returns 422; a duplicate sent while the original is still processing returns 409 with Retry-After. Keys are kept for 7 days and are scoped to your merchant account and endpoint. Requests without a key behave exactly as before.
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Conditional | Token from /tokens. Required unless charging with payment_method_id. |
payment_method_id | number | Conditional | Existing saved payment method id (CustomerDetails). Required unless charging with token. Aliases: x_payment_id, paymentmethodid. |
cvc | string | Conditional | Required for token-based charges. Provide at charge time (do not store). You can also pass billing.cvc. |
amount | number | Yes | Charge amount. |
transtype | string | No | Defaults to AUTH_CAPTURE. Use AUTH_ONLY for auth-then-capture flows. |
billing | object | No | Billing/contact fields (first_name, last_name, address, city, state, zip, country, phone, email, description). |
customer_id | number | No | Optional customer id. When charging with payment_method_id, if provided it must match the payment method’s customer. |
save_payment_method | boolean | No | If true, creates a customer/payment method (if needed) and charges it. Requires token-based charge (raw card data is needed). |
use_customer_profile | boolean | No | If true and customer_id is set, instructs the gateway to use the stored customer profile where supported. Auto-enabled for payment_method_id charges. |
x_login | string | No | Merchant login (only if not using Authorization header). |
x_tran_key | string | No | Merchant tran key/secret (only if not using Authorization header). |
{
"token": "tok_...",
"cvc": "123",
"amount": 12.34,
"transtype": "AUTH_CAPTURE",
"billing": {
"first_name": "Jane",
"last_name": "Smith",
"address": "123 Main St",
"city": "New York",
"state": "NY",
"zip": "10001",
"country": "USA",
"email": "jane@example.com",
"phone": "5550123",
"description": "Online Purchase"
}
}
{
"payment_method_id": 12345,
"customer_id": 67890,
"amount": 12.34,
"transtype": "AUTH_CAPTURE"
}
{
"token": "tok_...",
"cvc": "123",
"amount": 12.34,
"save_payment_method": true,
"billing": {
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com",
"company": "Acme Inc",
"address": "123 Main St",
"city": "New York",
"state": "NY",
"zip": "10001",
"country": "USA",
"phone": "5550123"
}
}
{
"result": "success",
"charge": {
"id": 123456,
"transaction_id": 98765,
"amount": 12.34,
"currency": "USD",
"status": 1,
"status_text": "succeeded",
"type": 1,
"created": "...",
"description": "Online Purchase",
"notes": null,
"reference_id": 0,
"last4": "4242"
},
"customer_id": 67890,
"payment_method_id": 12345,
"transaction": {
"status": 1,
"status_text": "Success",
"reason_text": "...",
"transaction_id": "123456"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | No | Optional capture amount (omit for full capture where supported). |
x_login | string | No | Merchant login (only if not using Authorization header). |
x_tran_key | string | No | Merchant tran key/secret (only if not using Authorization header). |
{
"amount": 12.34
}
| Code | Meaning | When it is returned |
|---|---|---|
200 | OK | Successful list, retrieve, search, update, and approved create/capture requests. Idempotent replays return the original response with this or the originally stored code. |
400 | Bad Request | Invalid JSON body; missing amount or token/payment_method_id; invalid or expired token; invalid card number, expiration, or CVC; missing cvc on a token-based charge; save_payment_method without a token; update with no supported fields; invalid Idempotency-Key. |
401 | Unauthorized | Missing or invalid credentials (Bearer token, Basic auth, or x_login/x_tran_key). |
403 | Forbidden | The authenticated merchant account is disabled. |
404 | Not Found | Charge, merchant, customer, or payment method not found (or not owned by the authenticated merchant); unrecognized route. |
405 | Method Not Allowed | HTTP method not supported on the route (e.g. DELETE /charges, GET /charges/{id}/capture). |
409 | Conflict | A request with the same Idempotency-Key is still processing. Includes a Retry-After header. |
422 | Unprocessable Entity | The gateway declined the charge or capture (see transaction.reason_text), or an Idempotency-Key was reused with a different payload. |
429 | Too Many Requests | Rate limit exceeded. Includes a Retry-After header and rate limit headers. |
500 | Internal Server Error | Server-side configuration or processing error (e.g. token key not configured). |
502 | Bad Gateway | The upstream payment gateway could not be reached. |
429 with Retry-After.description and notes only.amount and either token or payment_method_id.cvc (or billing.cvc). CVC is not stored in tokens.payment_method_id, customer_id is optional but (if provided) must match.save_payment_method requires a token-based charge.Transaction records are backed by the transactions table.
ID behavior: for GET /query/transaction/{id}, the endpoint first resolves
{id} against merchant-scoped presentation_id, then falls back to internal
transaction_id. In responses, uniqueID reflects merchant-facing
presentation_id.
POST /query/transaction creates a transaction record only. It does not run a gateway charge.
Use /charges to actually process a payment.
Raw card data (PAN/CVC/exp/routing/account) is rejected.
status is always forced to Unprocessed on create and cannot be set via this endpoint.
customer_id.
On create, if customer_id is omitted, the API will create a customer record using either a provided customer object or the provided billing/shipping fields.
Authentication:
- Authorization: Bearer <access_token> (recommended if you use the login/JWT flow)
- or Authorization: Basic (merchant x_login / x_tran_key)
- or include x_login and x_tran_key in the JSON body
Optional query parameters for GET /query/transaction:
page: page number (1-based). When provided, results are paginated.pageSize: records per page. Optional; defaults to 50.| Field | Type | Required | Description |
|---|---|---|---|
page | integer | No | Optional page number for list results. |
pageSize | integer | No | Optional page size for list results. |
filters | string | No | Optional filter expression (same behavior as other Expi list endpoints). |
sort | string | No | Optional sort key (for example -uniqueID). |
modifiers | string | No | Optional comma-separated response field projection. |
GET /query/transaction?page=1&pageSize=25&sort=-uniqueID
| Field | Type | Required | Description |
|---|---|---|---|
amount_total / amount | number | Yes | Transaction amount. amount_total matches the response structure. |
amount_tax | number | No | Optional tax amount. |
amount_discount | number | No | Optional discount amount. |
amount_shipping | number | No | Optional shipping amount. |
amount_surcharge | number | No | Optional surcharge amount. |
amount_tip | number | No | Optional tip amount. |
currency | string | No | Optional currency code (for example USD). |
description | string | No | Optional description. |
notes | string | No | Optional notes. |
coupon_id | number | No | Optional coupon id association. |
recurring_id | number | No | Optional recurring/subscription id association. |
transtype | string|number | No | Defaults to AuthCapture. Accepts string or numeric code. |
status | string|number | No | Not accepted on create. Always stored as Unprocessed. |
state | string|number | No | Defaults to Unknown. Accepts string or numeric code. |
source | string|number | No | Defaults to Endpoint. Accepts string or numeric code; response returns the human-readable label (for example, Endpoint). |
created / transdate | string | No | Optional transaction timestamp (if omitted, DB/defaults apply). created matches the response structure. |
invoicing_id / invoice_id | number | No | Optional link to an invoice. |
invoice | object | No | If provided and invoice id is omitted (invoicing_id / invoice_id), an invoice is created and linked. |
items / line_items | array | No | Optional line items. Stored on the linked invoice as invoice details. If no invoice context is supplied, an invoice will be created (requires customer_id or enough customer/billing data to create one). |
customer_id | number | No | Customer association. If omitted, a customer record is created from customer or billing/shipping. |
customer | object | No | Optional customer payload used to create a customer when customer_id is omitted. |
payment_information.method | number|string | No | Non-sensitive payment method indicator (ex: CC, echeck, or numeric code). |
payment_information.last4 | string | No | Non-sensitive last 4 digits (stored as provided digits only). |
billing | object | No | Optional billing/contact fields (first_name, last_name, address, city, state, zip, country, phone1, phone2, email). phone is also accepted as an alias for phone1. |
shipping | object | No | Optional shipping/contact fields (first_name, last_name, address, city, state, zip, country, phone1, phone2, email). phone is also accepted as an alias for phone1. |
{
"amount_total": 12.34,
"amount_tax": 0.50,
"amount_discount": 1.00,
"amount_shipping": 2.50,
"amount_surcharge": 0.25,
"amount_tip": 1.50,
"currency": "USD",
"description": "Order #1001",
"notes": "Recorded from mobile tap-to-pay",
"transtype": "AuthCapture",
"state": "Settled",
"created": "2026-01-29 12:34:56",
"payment_information": {
"method": "CC",
"last4": "4242"
},
"billing": {
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com",
"phone1": "555-555-5555"
},
"items": [
{"title": "T-Shirt", "quantity": 1, "unit_price": 12.34}
]
}
{
"result": "success",
"transaction": {
"uniqueID": 123456,
"customer_id": 555,
"coupon_id": 0,
"recurring_id": 0,
"amount_total": 12.34,
"amount_tax": 0,
"amount_discount": 12.34,
"amount_shipping": 0,
"amount_surcharge": 0,
"amount_tip": 0,
"currency": "USD",
"created": "2026-01-29 12:34:56",
"description": "Order #1001",
"notes": "Created by integration",
"billing": {
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com",
"phone1": "555-555-5555",
"phone2": "",
"address": "",
"address2": "",
"city": "",
"state": "",
"zip": "",
"country": ""
},
"shipping": {
"first_name": "",
"last_name": "",
"email": "",
"phone1": "",
"phone2": "",
"address": "",
"address2": "",
"city": "",
"state": "",
"zip": "",
"country": ""
},
"payment_information": {
"method": 16,
"last4": "4242"
},
"invoicing_id": null,
"reference_id": null,
"status": "Unprocessed",
"state": "Settled",
"transtype": "AuthCapture",
"source": "Endpoint"
}
}
Refund operations backed by Transactions. Refunds are processed as gateway CREDIT transactions against an existing charge (Transaction presentation_id) within the authenticated merchant scope.
Authentication:
- Authorization: Bearer <access_token> (recommended if you use the login/JWT flow)
- or Authorization: Basic (merchant x_login / x_tran_key)
- or include x_login and x_tran_key in the JSON body
Idempotency (optional):
Send an Idempotency-Key header (or idempotency_key body field, max 128 characters) on POST /refunds to safely retry a refund without crediting the card twice. If a request with the same key already completed, the original response is replayed with an Idempotency-Replayed: true header. Reusing a key with a different payload returns 422; a duplicate sent while the original is still processing returns 409 with Retry-After. Keys are kept for 7 days and are scoped to your merchant account and endpoint. Requests without a key behave exactly as before.
| Field | Type | Required | Description |
|---|---|---|---|
charge_id | number | Yes | Original charge Transaction presentation_id. Alias: transaction_id. |
amount | number | Yes | Refund amount. Must be greater than 0 and not exceed the original charge amount. |
notes | string | No | Optional notes sent to the gateway. |
x_login | string | No | Merchant login (only if not using Authorization header). |
x_tran_key | string | No | Merchant tran key/secret (only if not using Authorization header). |
{
"charge_id": 123456,
"amount": 12.34,
"notes": "Customer requested refund"
}
{
"result": "success",
"refund": {
"id": 222222,
"transaction_id": 98765,
"amount": 12.34,
"currency": "USD",
"status": 1,
"status_text": "succeeded",
"type": 4,
"created": "...",
"description": null,
"notes": null,
"reference_id": 0,
"last4": "4242"
},
"original_charge": {
"id": 123456,
"transaction_id": 12345,
"amount": 12.34,
"currency": "USD",
"status": 1,
"status_text": "succeeded",
"type": 1,
"created": "...",
"description": "Online Purchase",
"notes": null,
"reference_id": 0,
"last4": "4242"
},
"transaction": {
"status": 1,
"status_text": "Success",
"reason_text": "...",
"transaction_id": "222222"
}
}
| Code | Meaning | When it is returned |
|---|---|---|
200 | OK | The refund (gateway CREDIT) was approved. Idempotent replays return the original response with the originally stored code. |
400 | Bad Request | Missing charge_id/transaction_id; missing or non-numeric amount; amount not greater than 0; amount exceeds the original charge amount; invalid Idempotency-Key. |
401 | Unauthorized | Missing or invalid credentials (Bearer token, Basic auth, or x_login/x_tran_key). |
404 | Not Found | The referenced charge does not exist within the authenticated merchant scope; unrecognized route. |
405 | Method Not Allowed | Any HTTP method other than POST. |
409 | Conflict | A request with the same Idempotency-Key is still processing. Includes a Retry-After header. |
422 | Unprocessable Entity | The gateway declined the refund (see transaction.reason_text), or an Idempotency-Key was reused with a different payload. |
429 | Too Many Requests | Rate limit exceeded. Includes a Retry-After header and rate limit headers. |
500 | Internal Server Error | Server-side initialization or processing error. |
502 | Bad Gateway | The upstream payment gateway could not be reached. |
CREDIT transactions referencing the original charge id.429 with Retry-After.Chargebacks and retrievals (disputes) filed against your account. You can list disputes, retrieve a single dispute, and respond to an open dispute by accepting it or challenging it with evidence. Disputes are pulled from the chargeback processing system, so only merchants with a MID configured on their account can use this endpoint. Data and behavior match the Disputes page of the dashboard.
Authentication:
- Authorization: Bearer <access_token> (recommended if you use the login/JWT flow)
- or Authorization: Basic (merchant x_login / x_tran_key)
- or include x_login and x_tran_key in the JSON body
| Field | Type | Required | Description |
|---|---|---|---|
page | number | No | Page number. Defaults to 1. |
pageSize | number | No | Results per page, between 1 and 100. Defaults to 25. |
sort | string | No | Sort field, prefixed with - for descending. Supported fields: due_date, posted_date, case_amount, case_status, reason_code, id. Defaults to -due_date. |
filters | string | No | Standard filter expression (e.g. filters=case_status~=needs). Applied to the current page only, since disputes are paginated by the upstream system. |
modifiers | string | No | Comma-separated list of fields to include in each record (uniqueID is always included). |
{
"result": "success",
"disputes": [
{
"uniqueID": 448821,
"case_number": "7211930051",
"case_type": "Chargeback",
"case_status": "Needs Response",
"case_amount": 74.95,
"currency": "USD",
"reason_code": "10.4",
"reason_description": "Other Fraud - Card Absent Environment",
"cardholder_account_number": "************4242",
"posted_date": "2026-07-02",
"due_date": "2026-07-18",
"item_type": "Open"
}
],
"pageCount": 4,
"totalCount": 92
}
The list returns a summary of each dispute. Fields only available on the full record (e.g. card_brand, card_name, auth_code, arn, order_id, transaction_date, chargeback_date, notes) are omitted from list results — retrieve the dispute by id to get them.
Returns the full dispute record by its uniqueID. Disputes belonging to another merchant return 404.
{
"result": "success",
"dispute": {
"uniqueID": 448821,
"case_number": "7211930051",
"case_type": "Chargeback",
"case_status": "Needs Response",
"case_amount": 74.95,
"currency": "USD",
"reason_code": "10.4",
"reason_description": "Other Fraud - Card Absent Environment",
"card_brand": "Visa",
"card_name": "JOHN SMITH",
"cardholder_account_number": "************4242",
"auth_code": "081522",
"arn": "74537506123456789012345",
"order_id": "ORD-10592",
"transaction_date": "2026-06-02",
"chargeback_date": "2026-07-01",
"posted_date": "2026-07-02",
"due_date": "2026-07-18",
"item_type": "Open",
"notes": ""
}
}
Accepts liability for an open dispute. Only disputes that are open for responses (e.g. status Needs Response) can be accepted; otherwise 409 is returned.
| Field | Type | Required | Description |
|---|---|---|---|
comment | string | No | Optional comment recorded with the response. |
x_login | string | No | Merchant login (only if not using Authorization header). |
x_tran_key | string | No | Merchant tran key/secret (only if not using Authorization header). |
Challenges an open dispute with supporting evidence. Send the request as multipart/form-data with the evidence document in a file field. The file is required unless documents have already been uploaded to the dispute (e.g. through the dashboard). Allowed file types: JPG, PNG, GIF, PDF, TXT (max 32 MB).
| Field | Type | Required | Description |
|---|---|---|---|
file | file | Yes* | Evidence document (JPG, PNG, GIF, PDF, or TXT). *Optional only when the dispute already has uploaded documents. |
comment | string | No | Optional comment recorded with the challenge. |
x_login | string | No | Merchant login (only if not using Authorization header). |
x_tran_key | string | No | Merchant tran key/secret (only if not using Authorization header). |
curl -X POST https://api.example.com/query/disputes/448821/challenge \
-H "Authorization: Bearer <access_token>" \
-F "file=@signed_receipt.pdf" \
-F "comment=Customer signed for delivery on 2026-06-04"
{
"result": "success",
"message": "Challenge submitted",
"dispute": {
"uniqueID": 448821,
"case_status": "Under Review",
"item_type": "Resolved",
"...": "..."
}
}
Fields are included only when the upstream system provides them: list results contain the summary fields shown in the list sample above, while retrieving a dispute by id returns the full set below.
| Field | Type | Description |
|---|---|---|
uniqueID | number | Dispute identifier. Use with GET /query/disputes/{id}. |
case_number | string | Case number assigned by the processor. |
case_type | string | Type of case (e.g. Chargeback, Retrieval, Pre-Arbitration). |
case_status | string | Current status of the case (e.g. Needs Response, Under Review, Closed). |
case_amount | number | Disputed amount. |
currency | string | Currency of the disputed amount. |
reason_code | string | Card-network reason code (e.g. 10.4, 4837). |
reason_description | string | Human-readable description of the reason code. |
card_brand | string | Card network (Visa, Mastercard, etc.). |
card_name | string | Cardholder name, when provided by the network. |
cardholder_account_number | string | Masked card number. |
auth_code | string | Authorization code of the original transaction. |
arn | string | Acquirer Reference Number of the original transaction. |
order_id | string | Merchant order reference, when available. |
transaction_date | string | Date of the original transaction. |
chargeback_date | string | Date the chargeback was initiated. |
posted_date | string | Date the case was posted to your account. |
due_date | string | Deadline to respond to the case. |
item_type | string | Workflow state of the item: Open or Resolved. |
notes | string | Case notes, when available. |
| Code | Meaning | When it is returned |
|---|---|---|
200 | OK | The dispute list or dispute was returned, or the response (accept/challenge) was submitted successfully. |
400 | Bad Request | Invalid page/pageSize; unsupported sort field; missing, oversized, or invalid evidence file on a challenge. |
401 | Unauthorized | Missing or invalid credentials (Bearer token, Basic auth, or x_login/x_tran_key). |
404 | Not Found | The dispute does not exist or does not belong to the authenticated merchant. |
405 | Method Not Allowed | Any method/route combination other than the documented GET and POST routes. |
409 | Conflict | The dispute is not open for responses (accept/challenge on a closed or already-answered case). |
422 | Unprocessable Entity | The merchant account does not have a MID configured. |
502 | Bad Gateway | The chargeback processing system could not be reached or returned an error. |
503 | Service Unavailable | The dispute service is not configured for this account. |
filters are applied to the returned page only; use sort and pagination to narrow large result sets.Void operations backed by Transactions. Voids are processed as gateway VOID transactions against an existing charge (Transaction presentation_id) within the authenticated merchant scope.
Authentication:
- Authorization: Bearer <access_token> (recommended if you use the login/JWT flow)
- or Authorization: Basic (merchant x_login / x_tran_key)
- or include x_login and x_tran_key in the JSON body
| Field | Type | Required | Description |
|---|---|---|---|
charge_id | number | Conditional | Original charge Transaction presentation_id. Required unless id is supplied in the URL path. Alias: transaction_id. |
notes | string | No | Optional notes sent to the gateway. |
x_login | string | No | Merchant login (only if not using Authorization header). |
x_tran_key | string | No | Merchant tran key/secret (only if not using Authorization header). |
{
"charge_id": 123456,
"notes": "Customer requested cancellation"
}
{
"result": "success",
"void": {
"id": 333333,
"transaction_id": 99999,
"amount": 12.34,
"currency": "USD",
"status": 1,
"status_text": "succeeded",
"type": 8,
"created": "...",
"description": null,
"notes": null,
"reference_id": 0,
"last4": "4242"
},
"original_charge": {
"id": 123456,
"transaction_id": 12345,
"amount": 12.34,
"currency": "USD",
"status": 1,
"status_text": "succeeded",
"type": 1,
"created": "...",
"description": "Online Purchase",
"notes": null,
"reference_id": 0,
"last4": "4242"
},
"transaction": {
"status": 1,
"status_text": "Success",
"reason_text": "...",
"transaction_id": "333333"
}
}
VOID transactions referencing the original charge id.429 with Retry-After.409.Webhooks let a merchant register HTTPS endpoints that receive signed event payloads when payment activity happens. The API is available through /query/webhook, /query/webhooks, or /query/expiwebhook.
Authentication:
- Authorization: Bearer <access_token>
- or Authorization: Basic using merchant x_login / x_tran_key
- or include x_login and x_tran_key in the JSON body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS receiver URL. HTTP is only allowed when WEBHOOK_ALLOW_HTTP=true. |
events | array/string | No | Event types to deliver. Use ["*"] for all events. |
description | string | No | Internal label for the endpoint. |
enabled | boolean | No | Defaults to true. Disabled endpoints do not receive deliveries. |
{
"url": "https://example.com/webhooks/expitrans",
"description": "Production payment events",
"events": [
"charge.created",
"charge.captured",
"charge.failed",
"recurring.payment_succeeded",
"recurring.payment_failed",
"refund.created"
],
"enabled": true
}
{
"result": "success",
"webhook": {
"id": 101,
"object": "webhook_endpoint",
"url": "https://example.com/webhooks/expitrans",
"description": "Production payment events",
"enabled": true,
"events": ["charge.created", "charge.captured", "charge.failed", "recurring.payment_succeeded", "recurring.payment_failed", "refund.created"],
"api_version": "2026-06-29",
"created_at": "2026-06-29 12:00:00-06",
"updated_at": "2026-06-29 12:00:00-06",
"secret": "whsec_..."
}
}
| Event | When it fires |
|---|---|
charge.created | A charge succeeds through the Charges API. |
charge.updated | A charge description or notes field is updated. |
charge.captured | An authorized charge is captured. |
charge.failed | A charge or capture attempt returns a gateway failure and a transaction-backed charge exists. |
refund.created | A refund succeeds through the Refunds API. |
recurring.payment_succeeded | A recurring payment run succeeds. |
recurring.payment_failed | A recurring payment run fails or has a gateway transport error. |
recurring.completed | A recurring schedule completes after a successful final payment. |
webhook.test | A test event sent from the webhook API. |
Receivers get a JSON event object. Delivery includes Expi-Event-Id and Expi-Signature headers. The signature format is t={timestamp},v1={hmac}, where hmac is HMAC-SHA256(timestamp + "." + raw_body, endpoint_secret).
Failed deliveries are retried by the webhook delivery cron (lib/cron/webhooks.php) when their next_attempt_at time is due, up to the configured maximum attempt count. Manual resend marks a delivery due immediately so the same worker can process it.
{
"id": "evt_...",
"object": "event",
"type": "charge.created",
"created": 1782765600,
"data": {
"object": {
"id": 123456,
"transaction_id": 98765,
"amount": 12.34,
"currency": "USD",
"status_text": "succeeded"
}
}
}
Manage customer profiles with a single endpoint. Create customers and store contact, billing, and custom fields for downstream billing workflows.
| Field | Type | Required | Description |
|---|---|---|---|
page | integer | No | Optional page number for customer list results. |
pageSize | integer | No | Optional page size for customer list results. |
filters | string | No | Optional filter expression. |
sort | string | No | Optional sort key (for example -uniqueID). |
modifiers | string | No | Optional comma-separated response field projection. |
GET /query/customer?page=1&pageSize=25&sort=-uniqueID
{
"result": "success",
"customer": {
"uniqueID": 1234,
"customer_information": {
"firstname": "Alex",
"lastname": "Rivera",
"address1": "123 Market Street",
"address2": "",
"city": "San Francisco",
"state": "CA",
"zip": "94105",
"country": "USA",
"phone1": "4155550134",
"phone2": "",
"email": "alex.rivera@example.com"
},
"billing_information": {
"firstname": "Alex",
"lastname": "Rivera",
"address1": "123 Market Street",
"address2": "",
"city": "San Francisco",
"state": "CA",
"zip": "94105",
"country": "USA",
"phone": "4155550134",
"email": "alex.rivera@example.com"
},
"custom": {
"custom1": "A-1001",
"custom2": "Gold",
"custom3": "West",
"custom4": "",
"custom5": "",
"custom6": "",
"custom7": "",
"custom8": "",
"custom9": "",
"custom10": ""
},
"defaultPaymentID": 2002,
"customer_payments": [
{
"paymentID": 2001,
"paymenttype": "Credit Card",
"lastfour": "4242"
},
{
"paymentID": 2002,
"paymenttype": "Checking",
"lastfour": "6789"
}
]
}
}
{
"x_login": "...",
"x_tran_key": "...",
"customer": {
"customer_information": {
"firstname": "Jane",
"lastname": "Smith",
"email": "jane@example.com",
"phone1": "555-0123",
"address1": "123 Main St",
"city": "New York",
"state": "NY",
"zip": "10001",
"country": "US"
},
"billing_information": {
"firstname": "Jane",
"lastname": "Smith",
"address1": "123 Main St",
"city": "New York",
"state": "NY",
"zip": "10001",
"country": "US"
},
"custom": {
"custom1": "VIP Client"
}
}
}
{
"x_login": "...",
"x_tran_key": "...",
"_method": "PUT",
"customer": {
"customer_information": {
"firstname": "Jane",
"lastname": "Smith",
"email": "jane@example.com",
"phone1": "555-0123"
},
"billing_information": {
"address1": "456 Elm St",
"city": "New York",
"state": "NY",
"zip": "10001",
"country": "US"
},
"custom": {
"custom1": "VIP Client"
},
"customer_payments": [
{
"paymentID": 2002
}
]
}
}
Create and manage recurring billing schedules through the central query handler.
Create Modes: recurring creation supports single-product mode (product_id) and multi-product mode (products).
Required (Create): customer_id, payment_id, status, surcharge, run_transaction, and exactly one mode input: product_id OR products.
| Field | Type | Required | Description |
|---|---|---|---|
page | integer | No | Optional page number for recurring list results. |
pageSize | integer | No | Optional page size for recurring list results. |
filters | string | No | Optional filter expression. |
sort | string | No | Optional sort key (for example -uniqueID). |
modifiers | string | No | Optional comma-separated response field projection. |
GET /query/recurring?page=1&pageSize=25&sort=-uniqueID
{
"result": "success",
"data": {
"uniqueID": 12345,
"status": 1,
"interval": 3,
"interval_number": 1,
"run_until": 2,
"run_limit": 12,
"end_date": null,
"run_next": "2026-09-25 00:00:00",
"run_last": "2026-08-25 00:00:00",
"run_count": 2,
"run_total": 400.00,
"amount": 203.50,
"surcharge": 3.50,
"currency": "USD",
"customer_id": 6531,
"payment_id": 3370,
"idempotency_key": "recurring-single-20260725223646",
"items": [
{
"product_id": 823,
"product_name": "Monthly Service",
"description": "Monthly recurring service fee",
"qty": 1,
"price": 200
},
{
"product_id": null,
"product_name": "Custom Support Add-on",
"description": "Optional support fee",
"qty": 1,
"price": 0
}
]
}
}
| Field | Required | Notes |
|---|---|---|
customer_id | Yes | Customer for the schedule. |
payment_id | Yes | Stored payment method ID that belongs to the provided customer. |
status | Yes | Recurring status value. Allowed: 1=Active, 2=Completed, 4=Paused, 5=Terminated. |
surcharge | Yes | Numeric surcharge added to computed recurring amount. |
run_transaction | Yes | When true, executes Recurring::runRecurring() after save. |
idempotency_key | Conditional | Client-generated unique string, up to 128 characters. Required when run_transaction is true. Reuse the same key when retrying the same create request; if a recurring already exists for that merchant and key, the endpoint returns the existing recurring instead of creating another schedule. |
product_id | Mode | Single-product mode. Must not be sent together with root products. |
products | Mode | Multi-product mode. Array/object of items (normalized internally). Must not be sent with root product_id. |
recurring_rule_product | Multi Mode | Optional schedule anchor. If provided, it must belong to merchant, be recurring-enabled, and be present in products. |
interval | No | Optional override: 1=Day, 2=Week, 3=Month, 4=Year. |
interval_number | No | Optional override; must be >= 1. |
run_until | No | Optional override: 0=Until terminated, 1=Specific date, 2=Fixed count. |
run_limit | Conditional | Required when run_until is 2 (count). |
end_date | Conditional | Required when run_until is 1. Must be a future date based on the merchant-local date; date/time value is accepted as provided. |
start_date | No | Date/time value is accepted as provided. Defaults to current date/time when omitted. |
run_last | No | Date/time value is accepted as provided. |
run_next | No | Must be a future date based on the merchant-local date when provided; date/time value is accepted as provided. Defaults to merchant-local today + 1 day when omitted. |
run_total | No | Optional decimal, defaults to 0. |
run_count | No | Optional whole number, defaults to 0. |
Validation Notes (Create):
run_next and date-based end_date must be future dates based on the merchant-local date; today's merchant-local date is not accepted.idempotency_key is created by the client and should be unique per create request for the merchant. Reuse it only when retrying the same request after an unclear response.x_login.product_id (or alias productID). For custom items without a product ID, name and price are required; qty defaults to 1.recurring_rule_product is provided, its recurring rule is used.products, that product's recurring rule is used.recurring_rule_product or explicit schedule fields.amount = product price + surchargeamount = sum(qty * unit_price) + surcharge{
"x_login": "...",
"x_tran_key": "...",
"recurring": {
"customer_id": 123,
"payment_id": 456,
"product_id": 111,
"status": 1,
"surcharge": 3.50,
"run_transaction": true,
"idempotency_key": "recurring-single-20260725223646",
"run_next": "2026-08-25",
"interval": 2,
"interval_number": 1,
"run_until": 2,
"run_limit": 12
}
}
{
"x_login": "...",
"x_tran_key": "...",
"recurring": {
"customer_id": 123,
"payment_id": 456,
"recurring_rule_product": 111,
"products": [
{
"product_id": 111
},
{
"productID": 222,
"qty": 2
},
{
"name": "Custom Item",
"description": "Manual line item",
"qty": 1,
"price": 12.50
}
],
"status": 1,
"surcharge": 3.50,
"run_transaction": false,
"idempotency_key": "recurring-multi-20260725223646",
"interval": 2,
"interval_number": 1,
"run_until": 1,
"end_date": "2027-01-25"
}
}
{
"x_login": "...",
"x_tran_key": "...",
"recurring": {
"customer_id": 123,
"payment_id": 456,
"products": [
{
"name": "Custom Item A",
"qty": 1,
"price": 15.00
},
{
"name": "Custom Item B",
"qty": 2,
"price": 20.00
}
],
"status": 1,
"surcharge": 3.50,
"run_transaction": false,
"interval": 3,
"interval_number": 1,
"run_until": 2,
"run_limit": 6
}
}
| Field | Required | Description |
|---|---|---|
uniqueID | No | Optional when calling /query/recurring/{id}; otherwise required. |
status | No | 1=Active, 2=Completed, 4=Paused, 5=Terminated. |
interval | No | 1=Day, 2=Week, 3=Month, 4=Year. |
interval_number | No | Number of intervals between runs. |
run_until | No | 0=Until terminated, 1=Specific date, 2=Fixed count. |
run_limit | No | Required when run_until is 2 (count). |
end_date | No | Required when run_until is 1; must be a future date based on the merchant-local date when provided. |
run_next | No | Next run date/time; must be a future date based on the merchant-local date when provided. |
surcharge | No | Surcharge amount added to product price. |
payment_id | No | Stored payment method ID. |
Note: Recurring line items cannot be updated via the update endpoint.
{
"x_login": "...",
"x_tran_key": "...",
"_method": "PUT",
"recurring": {
"status": 1,
"interval": 2,
"interval_number": 1,
"run_until": 2,
"run_limit": 12,
"run_next": "2026-09-01"
}
}
When a scheduled recurring payment run fails (card declined or gateway transport error), the schedule itself is not changed:
status remains 1 (Active); the schedule is not paused or terminated automatically.run_next, run_count, and run_total are not advanced. The schedule only advances after a successful payment.status is changed (for example to 4=Paused or 5=Terminated via the update endpoint).status, or update payment_id to a valid stored payment method so the next retry can succeed.recurring.payment_failed webhook event is emitted for each failed attempt. When a retry later succeeds, recurring.payment_succeeded is emitted and the schedule advances normally. See the Webhooks documentation for event details.Create and manage invoices.
| Field | Type | Required | Description |
|---|---|---|---|
page | integer | No | Optional page number for invoice list results. |
pageSize | integer | No | Optional page size for invoice list results. |
filters | string | No | Optional filter expression. |
sort | string | No | Optional sort key (for example -uniqueID). |
modifiers | string | No | Optional comma-separated response field projection. |
GET /query/invoice?page=1&pageSize=25&sort=-uniqueID
Supports optional invoice filters:
| Filter | Description |
|---|---|
customer_id | Limit to a specific customer |
status | Invoice status (e.g., Pending, Sent, Paid) |
due_before | Invoices due before date (YYYY-MM-DD) |
due_after | Invoices due after date (YYYY-MM-DD) |
Returns a single invoice by unique ID. If not found, returns 404.
Note: Provide customer_id to associate with an existing customer. If customer_id is omitted, include a customer object to create a new customer.
{
"x_login": "...",
"x_tran_key": "...",
"invoice": {
"invoice_number": "INV-2024-001",
"due_date": "2024-12-31",
"notes": "Thank you for your business",
"discount": 0,
"tax": 10.50,
"shipping": 5.00,
"amount": 635.50,
"customer_id": 123,
"items": [
{
"title": "Web Design",
"description": "Homepage design",
"quantity": 1,
"unit_price": 500.00
},
{
"title": "Hosting",
"quantity": 12,
"unit_price": 10.00
}
]
}
}
{
"x_login": "...",
"x_tran_key": "...",
"_method": "PUT",
"invoice": {
"uniqueID": 456,
"notes": "Updated notes",
"status": "Sent"
}
}
DELETE /query/invoice/456
Manage products and services.
Returns a list of products. If none exist, returns an empty list: { "products": [] } with status 200.
Optional query parameters for GET /query/product:
page: page number (1-based). When provided, results are paginated.pageSize: records per page. Optional; defaults to 50.| Field | Type | Required | Description |
|---|---|---|---|
page | integer | No | Optional page number for product list results. |
pageSize | integer | No | Optional page size for product list results. |
filters | string | No | Optional filter expression. |
sort | string | No | Optional sort key (for example -uniqueID). |
modifiers | string | No | Optional comma-separated response field projection. |
GET /query/product?page=1&pageSize=25&sort=-uniqueID
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Product name. |
description | string | No | Product description. |
price | number | No | Product price. |
isBookable | boolean | No | Marks product as bookable. |
duration | integer | No | Duration in minutes (for bookable products). |
is_recurring | boolean | No | Enable recurring schedule; provide recurring_rule when true. |
recurring_rule |
object | Conditional |
Recurring schedule config; required when is_recurring is true.
Format: {"interval":"1","interval_number":"1","run_limit":"7"}.
interval: 1=Day, 2=Week, 3=Month, 4=Year. interval_number: how many intervals between charges (e.g., 2 with interval=Week means every 2 weeks). run_limit: number of times to run; omit or use 0 for unlimited. |
{
"x_login": "...",
"x_tran_key": "...",
"product": {
"name": "Premium Service",
"description": "One hour consultation",
"price": 99.99,
"isBookable": true,
"duration": 60,
"is_recurring": true,
"recurring_rule": {
"interval": "1",
"interval_number": "1",
"run_limit": "7"
}
}
}
| Field | Type | Required | Description |
|---|---|---|---|
uniqueID | integer | No | Optional when calling /query/product/{id}; otherwise required. |
name | string | No | Product name. |
description | string | No | Product description. |
price | number | No | Product price. |
isBookable | boolean | No | Marks product as bookable. |
duration | integer | No | Duration in minutes (for bookable products). |
is_recurring | boolean | No | Enable recurring schedule; provide recurring_rule when true. |
recurring_rule |
object | Conditional |
Recurring schedule config; required when is_recurring is true.
Format: {"interval":"1","interval_number":"1","run_limit":"7"}.
interval: 1=Day, 2=Week, 3=Month, 4=Year. interval_number: how many intervals between charges (e.g., 2 with interval=Week means every 2 weeks). run_limit: number of times to run; omit or use 0 for unlimited. |
Notes:
- Omit fields you do not want to change.
- When setting is_recurring to false, the recurring schedule is cleared.
{
"x_login": "...",
"x_tran_key": "...",
"_method": "PUT",
"product": {
"name": "Updated Service Name",
"price": 149.99,
"isBookable": true,
"duration": 60,
"is_recurring": false,
"recurring_rule": {
"interval": "1",
"interval_number": "1",
"run_limit": "7"
}
}
}
Manage discount coupons.
Returns a list of coupons. If none exist, returns an empty list: { "coupons": [] } with status 200.
Optional query parameters for GET /query/coupon:
page: page number (1-based). When provided, results are paginated.pageSize: records per page. Optional; defaults to 50.| Field | Type | Required | Description |
|---|---|---|---|
page | integer | No | Optional page number for coupon list results. |
pageSize | integer | No | Optional page size for coupon list results. |
filters | string | No | Optional filter expression. |
sort | string | No | Optional sort key (for example -uniqueID). |
modifiers | string | No | Optional comma-separated response field projection. |
GET /query/coupon?page=1&pageSize=25&sort=-uniqueID
Create accepts either a nested coupon object or a flat top-level payload.
name is required and cannot be blank.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Coupon name. |
description | string | No | Coupon description. |
code | string | No | Coupon code customers enter. |
percent_off | number | No | Percent discount (e.g., 10 for 10% off). |
duration_type | string | No | Discount duration (e.g., once). |
first_purchase_only | boolean | No | Only apply to the first purchase. |
max_redemptions | integer | No | Maximum number of redemptions allowed. |
enabled | boolean | No | Enable/disable coupon. |
{
"x_login": "...",
"x_tran_key": "...",
"coupon": {
"name": "New Customer Discount",
"description": "10% off first purchase",
"code": "WELCOME10",
"percent_off": 10,
"duration_type": "once",
"first_purchase_only": true,
"max_redemptions": 100,
"enabled": true
}
}
{
"x_login": "...",
"x_tran_key": "...",
"name": "New Customer Discount",
"code": "WELCOME10",
"percent_off": 10,
"duration_type": "once",
"first_purchase_only": true,
"max_redemptions": 100,
"enabled": true
}
Update accepts either a nested coupon object or a flat top-level payload.
uniqueID is optional when calling /query/coupon/{id}, and required for non-path updates.
| Field | Type | Required | Description |
|---|---|---|---|
uniqueID | integer | No | Optional when calling /query/coupon/{id}; otherwise required. |
name | string | No | Coupon name. |
description | string | No | Coupon description. |
code | string | No | Coupon code customers enter. |
percent_off | number | No | Percent discount. |
duration_type | string | No | Discount duration. |
first_purchase_only | boolean | No | Only apply to the first purchase. |
max_redemptions | integer | No | Maximum number of redemptions allowed. |
enabled | boolean | No | Enable/disable coupon. |
{
"x_login": "...",
"x_tran_key": "...",
"_method": "PUT",
"coupon": {
"name": "Updated Coupon Name",
"percent_off": 15,
"enabled": true
}
}
DELETE /query/coupon/123
Appointment Scheduling via the Payment Gateway
The Booking Module lets merchants manage appointment-based services, staff, schedules, and customer bookings — all through the payment gateway's standard /query/booking endpoint. The gateway handles authentication, resolves the caller's identity, mints a module JWT, and forwards the request to the booking backend.
All booking calls go through: /query/booking/{path}
All responses follow the ExpiEndpoint envelope — the payload is returned under a named resource key alongside "result":
{ "result": "success", "booking": { ... } }
{ "result": "success", "bookings": [ ... ] }
{ "result": "success", "service": { ... } }
{ "result": "success", "staff": [ ... ] }
Request bodies must be wrapped under the resource name. Flat JSON (fields at the top level) is also accepted:
// wrapped (documented format)
{ "booking": { "scheduledAt": "2027-06-01T09:00Z", "timeZone": "UTC", ... } }
// flat (also accepted)
{ "scheduledAt": "2027-06-01T09:00Z", "timeZone": "UTC", ... }
Every endpoint except the manage token flows requires the caller to authenticate. The gateway resolves identity in order:
Authorization: Bearer <token> with a sub or user_id claim.Authorization: Basic base64(merchanttext:secret), the merchant's existing API credential (the same one used for x_login/x_tran_key elsewhere in the gateway).The resolved user's account type determines the booking permission forwarded to the backend:
merchant, enhanced, sysad account types → merchant permissionBasic Auth alone (no session/Bearer user) also resolves to merchant permission by default. A handful of flows scope down to public permission when they explicitly opt in — the anonymous booking-create flow, and top-level reads of staff, services, and merchant settings — see the next section.
These flows are intended for the unauthenticated customer-facing widget. They still require the merchant's Basic Auth credential — there is no bare merchant_id fallback — but they run with a narrower public permission instead of full merchant permission:
GET /query/booking/slots — available time slotsPOST /query/booking with is_public_booking: true in the body — create a booking (public/customer-facing). Omitting the flag (e.g. internal/migration callers using the same merchant credential) resolves to full merchant permission instead.GET /query/booking/staff, GET /query/booking/staff/{staffId}, GET /query/booking/services, and GET /query/booking/merchant with ?is_public=true — top-level reads only (no sub-resource path segment). Omitting the flag resolves to full merchant permission instead. Each endpoint's response is narrowed for public callers — see that endpoint's section below for the exact fields returned.The /query/booking/manage/{token}/... flows remain fully unauthenticated — they are gated solely by the signed manage token in the URL.
Returns available start times for a service on a given calendar date. Requires the merchant's Basic Auth credential (runs with public permission); there is no bare merchant_id fallback.
| Field | Type | Required | Description |
|---|---|---|---|
serviceId | UUID | Yes | Service to evaluate. |
staffId | UUID | Conditional | Required when the service uses providerMode = atCreation; must be omitted for later. |
date | string YYYY-MM-DD | Yes | Local calendar date to query. |
displayTz | string | No | Optional. Return slot times converted into this timezone instead of the merchant's own. Defaults to the merchant's timezone when omitted. |
quantity | integer | No | Number of seats. Defaults to 1. |
GET /query/booking/slots?serviceId=uuid&staffId=uuid&date=2027-06-01&displayTz=America%2FLos_Angeles
Authorization: Basic base64(merchanttext:secret)
{
"result": "success",
"slots": [
{ "time": "09:00" },
{ "time": "10:00" },
{ "time": "14:00" }
]
}
Creates a booking. Requires the merchant's Basic Auth credential (or an authenticated merchant/staff session). Pass is_public_booking: true in the body for the customer-facing widget flow — this scopes the request to public permission instead of full merchant permission. Omit it for internal/migration callers that need full merchant permission.
Staff callers must have canCreateBooking = true. Set paymentMethod to online when the customer will pay via the payment gateway; the booking is created in a pending state and confirmed later via confirm-payment.
| Field | Type | Required | Description |
|---|---|---|---|
is_public_booking | boolean | No | Set true for customer-widget bookings to scope to public permission. Defaults to false (full merchant permission). |
serviceId | UUID | Yes | Service being booked. |
staffId | UUID | Conditional | Required for atCreation services; must be omitted for later. |
scheduledAt | ISO 8601 datetime | Yes | Booking start time (minute precision). |
timeZone | string | Yes | Timezone identifier. |
paymentMethod | inPerson | online | Yes | Payment method chosen by the customer. |
customerExternalId | integer | Yes | Gateway customer/user ID. |
customerFname | string | Yes | Customer first name (1–25 chars). |
customerLname | string | Yes | Customer last name (1–25 chars). |
customerEmail | string | Conditional | Required when emailRequired is set to true on the merchant. |
customerPhone | string | Yes | E.164 phone if provided. |
taxRate | number | No | Tax percentage (non-negative). |
surchargeType | flat | percentage | Conditional | Required together with surchargeAmount. |
surchargeAmount | number | Conditional | Required together with surchargeType. |
quantity | integer | No | Seats/units. Defaults to 1. |
notes | string | No | Internal notes. |
{
"is_public_booking": true,
"booking": {
"serviceId": "uuid",
"staffId": "uuid",
"scheduledAt": "2027-06-01T09:00Z",
"timeZone": "UTC",
"paymentMethod": "online",
"customerExternalId": 5001,
"customerFname": "Jane",
"customerLname": "Doe",
"customerEmail": "jane@example.com",
"customerPhone": "+15551234567",
"taxRate": 8,
"quantity": 1
}
}
{
"result": "success",
"booking": {
"id": "uuid",
"serviceId": "uuid",
"staffId": "uuid",
"scheduledAt": "2027-06-01T09:00:00.000Z",
"endsAt": "2027-06-01T10:00:00.000Z",
"paymentMethod": "online",
"paymentStatus": "pending",
"status": "pending",
"snapshotPrice": "30.00",
"snapshotDurationMinutes": 60,
"totalPrice": "30.00",
"manageToken": "jwt"
}
}
manageToken from the response. It is the only credential that allows the customer to self-service cancel or reschedule the booking via the manage endpoints below.
Returns bookings for the authenticated merchant. Staff callers without canViewAllBookings are scoped to their own bookings automatically.
{
"result": "success",
"bookings": [
{
"id": "uuid",
"serviceId": "uuid",
"staffId": "uuid",
"scheduledAt": "2027-06-01T09:00:00.000Z",
"endsAt": "2027-06-01T10:00:00.000Z",
"status": "pending",
"paymentMethod": "online",
"paymentStatus": "pending",
"totalPrice": "32.40"
}
]
}
| Field | Type | Required | Description |
|---|---|---|---|
date | string YYYY-MM-DD | No | Filter by date; must be paired with tz. |
tz | string | No | Timezone; must be paired with date. |
status | enum | No | pending, confirmed, cancelled, completed, noShow |
staffId | UUID | No | Filter by staff. Merchants only; non-admin staff are always scoped to themselves. |
serviceId | UUID | No | Filter by service. |
customerExternalId | integer | No | Filter by customer. |
paymentMethod | enum | No | inPerson or online |
paymentStatus | enum | No | pending, paid, failed, refunded |
Returns a single booking. Staff callers without canViewAllBookings can only view bookings assigned to themselves.
{
"result": "success",
"booking": {
"id": "uuid",
"serviceId": "uuid",
"staffId": "uuid",
"scheduledAt": "2027-06-01T09:00:00.000Z",
"endsAt": "2027-06-01T10:00:00.000Z",
"status": "pending",
"paymentMethod": "online",
"paymentStatus": "pending",
"snapshotName": "Haircut",
"snapshotPrice": "30.00",
"snapshotDurationMinutes": 60,
"totalPrice": "32.40"
}
}
Updates a booking. Callable by merchant or staff permission. Staff callers must have canManageBooking = true, and are rejected outright if the request body contains a staffId key at all — not just when it would actually change the value. Only merchant callers may include staffId.
| Field | Type | Description |
|---|---|---|
status | enum | confirmed, completed, noShow, cancelled |
paymentStatus | enum | Only paid is accepted; only for inPerson bookings. |
notes | string | Free-text notes. |
staffId | UUID | Merchant only — including this key at all gets a staff caller rejected. Must reference an active staff member linked to the service, else 404. See Override Behavior below for what an explicit value does on a later-mode service. |
scheduledAt | ISO 8601 datetime | Must be paired with timeZone. |
timeZone | string | Must be paired with scheduledAt. |
quantity | integer | Positive integer. |
{
"booking": {
"status": "confirmed",
"notes": "Arrived 5 min early",
"scheduledAt": "2027-06-01T10:00Z",
"timeZone": "America/New_York",
"staffId": "uuid",
"quantity": 2
}
}
{
"result": "success",
"booking": {
"id": "uuid",
"status": "confirmed",
"notes": "Arrived 5 min early",
"scheduledAt": "2027-06-01T09:00:00.000Z"
}
}
| Status | Cause |
|---|---|
200 | Booking updated. |
400 | paymentStatus: 'paid' requested for a booking whose paymentMethod isn't inPerson ("Online payments must be confirmed through the payment gateway."), or no recognized field changed at all ("No valid fields provided."). |
401 | Missing or invalid token. |
403 | Staff caller lacks canManageBooking; staff caller included a staffId key in the body at all; or the caller's permission isn't merchant/staff. |
404 | Merchant, staff record, or booking not found; or an explicit staffId doesn't reference an active staff member linked to the booking's service. |
409 | Booking is already cancelled; paymentStatus: 'paid' requested but payment was already processed; or the resulting time slot isn't available (see Override Behavior — this single message covers several distinct causes). |
422 | Body failed schema validation (see the validation-errors note near the bottom of this page). |
atCreation services, when this endpoint changes scheduledAt/quantity and triggers a slot-availability recheck, the assigned staff member's durationMinutesOverride is used for that check (and to recompute endsAt) instead of the service's base durationMinutes — but only when the merchant has allowStaffPricingOverrides = true (a boolean flag on the merchant's own account, not a separate settings resource) and an override is actually set for that staff/service pairing. Otherwise the service's base duration is used.
providerMode = later bookings: if the request changes scheduledAt or quantity and does not include a staffId key at all, fill-first assignment re-runs against the new slot/quantity and overwrites staffId. The outcome is one of three things — a specific staff member is assigned when one clearly has the most remaining capacity; staffId is set back to null when there's remaining capacity but multiple staff are tied for it (no clear winner, left for a merchant to assign manually); or, if no staff member has any remaining capacity at all, the request is rejected with 409 rather than silently clearing the assignment.
providerMode = later bookings: if the request includes an explicit, non-null staffId — whether or not scheduledAt/quantity also changed — that specific staff member's own schedule, breaks, exceptions, existing bookings, and capacity are checked at the resulting time (the new scheduledAt if provided, otherwise the booking's current one). This is a completely different, narrower check than the aggregate "is anyone free" check used elsewhere — being merely linked to the service isn't enough, they have to actually be free themselves. 409 if they aren't. There is no short-circuit for resubmitting the staff member who's already assigned: the same specific-availability check runs every time staffId is present, even when the value is unchanged.
409s from this endpoint — the aggregate reschedule-target check, fill-first finding no staff with capacity, and the per-staff pin check above — return the exact same message string. There is no way to distinguish which of the three occurred from the response alone.
Marks an online booking as paid after the payment gateway confirms the transaction. Merchant only. Idempotent when called with the same transaction ID.
| Field | Type | Required | Description |
|---|---|---|---|
transactionExternalId | string | Yes | Gateway transaction identifier. |
{
"booking": {
"transactionExternalId": "txn_abc123"
}
}
Marks an online payment as failed and cancels the booking. Merchant only. Idempotent when the booking is already in a failed state.
These endpoints are authenticated solely by the signed manage token returned at booking creation — no gateway session or credentials are needed.
Returns the booking addressed by the manage token and flags whether self-service cancel and reschedule are still permitted.
{
"result": "success",
"booking": {
"id": "uuid",
"scheduledAt": "2027-06-01T09:00:00.000Z",
"status": "pending",
"canCancel": true,
"canReschedule": true
}
}
Cancels the booking. Returns 409 if the booking is already cancelled, in a non-cancellable state, or outside the cancellation window configured by the merchant.
Reschedules the booking to a new slot. A new manageToken is returned — the old one is invalidated.
| Field | Type | Required | Description |
|---|---|---|---|
scheduledAt | ISO 8601 datetime | Yes | New start time. |
timeZone | string | Yes | Timezone identifier. |
{
"booking": {
"scheduledAt": "2027-06-02T10:00Z",
"timeZone": "UTC"
}
}
{
"result": "success",
"booking": {
"id": "uuid",
"scheduledAt": "2027-06-02T10:00:00.000Z",
"status": "pending",
"manageToken": "jwt"
}
}
manageToken is returned after every reschedule. The previous token is invalidated.
Merchant-only endpoints. Manage booking settings, weekly opening hours, and one-off date exceptions.
Returns the merchant's booking configuration together with the full weekly schedule and all date exceptions. Accessible by merchant and staff tokens. Also accessible with public permission via ?is_public=true (see Authentication above) — public callers receive a narrowed response with just serviceLabel, slotFormat, emailRequired, timeBeforeBooking, acceptsInPersonPayment, and acceptsOnlinePayment; no schedule, exceptions, or identifying fields.
Registers or updates the merchant's booking record. Safe to call on every login — creates on first call, updates on subsequent calls. All fields are optional.
| Field | Type | Default | Description |
|---|---|---|---|
acceptsInPersonPayment | boolean | false | Accept in-person payments (e.g. cash). |
acceptsOnlinePayment | boolean | false | Accept online payments (e.g. card). |
bookingService | boolean | false | Enable service booking module. |
bookingEvent | boolean | false | Enable event booking module. |
serviceLabel | string | "" | Custom UI label for services. |
eventLabel | string | "" | Custom UI label for events. |
staffLabel | string | "" | Custom UI label for staff. |
timeStepMinutes | integer | 15 | Slot granularity in minutes. |
timeBeforeBooking | integer | 1 | Minimum days in advance a booking can be made. |
leadTimeMinutes | integer | 60 | Minimum minutes of notice before a booking. |
cancelWindowHours | integer | 24 | Hours before scheduledAt when self-cancel/reschedule stop being allowed. |
allowStaffPricingOverrides | boolean | false | Enable per-staff price and duration overrides on services. |
slotFormat | twentyFourHour | twelveHour | "twelveHour" | Frontend-only display preference for how time slots are rendered (e.g. `14:00` vs `2:00 PM`). Not used by the backend for any calculation |
emailRequired | boolean | false | When true, POST /api/v1/bookings requires both customerEmail and customerPhone. When false, only customerPhone is required and customerEmail is optional |
{
"merchant": {
"acceptsInPersonPayment": true,
"acceptsOnlinePayment": true,
"bookingService": true,
"serviceLabel": "Appointment",
"timeStepMinutes": 30,
"leadTimeMinutes": 120,
"cancelWindowHours": 24,
"allowStaffPricingOverrides": false
}
}
Returns the merchant's weekly opening hours as an array of schedule entries. Returns an empty array if no schedule has been set yet. Accessible by merchant and staff tokens.
{
"result": "success",
"schedule": [
{ "id": "uuid", "dayOfWeek": 0, "isOpen": false, "openTime": null, "closeTime": null },
{ "id": "uuid", "dayOfWeek": 1, "isOpen": true, "openTime": "09:00", "closeTime": "18:00" }
]
}
Replaces the merchant's full weekly opening hours. Must send all 7 days (one entry per day of week, 0 = Sunday through 6 = Saturday). Each day's record is upserted.
| Field | Type | Required | Description |
|---|---|---|---|
dayOfWeek | integer 0–6 | Yes | 0 = Sunday, 6 = Saturday. |
isOpen | boolean | Yes | Whether the merchant is open that day. |
openTime | string HH:MM | Conditional | Required when isOpen is true. |
closeTime | string HH:MM | Conditional | Required when isOpen is true. |
{
"schedule": [
{ "dayOfWeek": 0, "isOpen": false },
{ "dayOfWeek": 1, "isOpen": true, "openTime": "09:00", "closeTime": "18:00" },
{ "dayOfWeek": 2, "isOpen": true, "openTime": "09:00", "closeTime": "18:00" },
{ "dayOfWeek": 3, "isOpen": true, "openTime": "09:00", "closeTime": "18:00" },
{ "dayOfWeek": 4, "isOpen": true, "openTime": "09:00", "closeTime": "18:00" },
{ "dayOfWeek": 5, "isOpen": true, "openTime": "09:00", "closeTime": "14:00" },
{ "dayOfWeek": 6, "isOpen": false }
]
}
Returns all date exceptions for the merchant. Returns an empty array if none have been created. Accessible by merchant and staff tokens.
{
"result": "success",
"exceptions": [
{ "id": "uuid", "date": "2026-12-25", "isOpen": false, "openTime": null, "closeTime": null }
]
}
Returns a single date exception by ID. Accessible by merchant and staff tokens.
Adds or updates a one-off date override (e.g. a holiday or special hours). Idempotent — calling again with the same date updates the existing record.
| Field | Type | Required | Description |
|---|---|---|---|
date | string YYYY-MM-DD | Yes | The date to override. |
isOpen | boolean | Yes | false = closed all day. true = open with special hours. |
openTime | string HH:MM | Conditional | Required when isOpen is true. |
closeTime | string HH:MM | Conditional | Required when isOpen is true. |
Removes a date exception. Only the owning merchant can delete it.
Services are the bookable items in the catalog. Merchants and authorized staff can create, update, and delete services. Staff access is governed by per-staff permission flags.
Creates a service record. Merchant and staff callers with canCreateService = true may call this endpoint.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Service/product name. |
price | number | Yes | Service price (positive). |
description | string | No | Service description. |
durationMinutes | integer | Yes | How long the service takes (positive integer). |
capacity | integer | No | Max simultaneous bookings per slot. Omit for unlimited. |
capacityMode | perStaff | perService | No | Defaults to perStaff. Whether capacity is enforced per staff member or shared across all staff for the slot. Only meaningful when capacity is set. |
monday–sunday | boolean | No | Days of the week when the service is offered. All default to false. |
schedulingMode | blocks | startTime | No | Defaults to blocks. Use startTime for a fixed daily start time. |
fixedStartTime | string HH:MM | Conditional | Required when schedulingMode is startTime. |
providerMode | atCreation | later | No | Defaults to atCreation. later defers staff assignment after booking. |
{
"service": {
"name": "Swedish Massage",
"price": 75.00,
"description": "Relaxing 60-minute massage",
"durationMinutes": 60,
"capacity": 5,
"capacityMode": "perStaff",
"monday": true,
"tuesday": true,
"wednesday": true,
"thursday": true,
"friday": true,
"schedulingMode": "blocks",
"providerMode": "atCreation"
}
}
Returns all services for the merchant. Staff callers with canViewAllServices = false only see services they are assigned to. Also accessible with public permission via ?is_public=true — public callers always receive all merchant services (no assignment scoping), with each service's staff array reduced to { id, firstName, lastName, color } plus effectivePrice/effectiveDurationMinutes (the override value when one applies, otherwise the service's base price/durationMinutes) in place of the raw priceOverride/durationMinutesOverride fields.
Returns a single service with its assigned staff. Staff callers without canViewAllServices receive 404 if they are not assigned to the requested service.
Updates a service. Staff require canEditService = true. Staff without canViewAllServices can only edit services they are assigned to.
| Field | Type | Description |
|---|---|---|
name | string | Service name. |
price | number | Service price (positive). |
description | string | Service description. |
durationMinutes | integer | Duration in minutes (positive integer). |
capacity | integer | Positive integer. Send empty string to revert to unlimited. |
capacityMode | perStaff | perService | Whether capacity is enforced per staff member or shared across all staff for the slot. Only meaningful when capacity is set. |
monday–sunday | boolean | Days the service is offered. |
schedulingMode | blocks | startTime | |
fixedStartTime | string HH:MM | Required when switching to startTime mode. Cleared automatically when switching back to blocks. |
providerMode | atCreation | later |
{
"service": {
"name": "Deep Tissue Massage",
"price": 75.00,
"description": "60-minute deep tissue session.",
"durationMinutes": 60,
"capacity": 1,
"capacityMode": "perService",
"monday": true,
"tuesday": true,
"wednesday": true,
"thursday": true,
"friday": true,
"saturday": false,
"sunday": false,
"schedulingMode": "blocks",
"providerMode": "atCreation"
}
}
Permanently deletes a service. Staff require canDeleteService = true. Staff without canViewAllServices can only delete services they are assigned to.
Replaces the full list of staff assigned to a service. Sending an empty array removes all staff. Merchant only.
| Field | Type | Required | Description |
|---|---|---|---|
staffIds | UUID[] | Yes | Array of staff UUIDs to assign. All must belong to the merchant. |
{
"staffService": {
"staffIds": ["uuid-1", "uuid-2"]
}
}
{
"result": "success",
"staffServices": [
{ "staffId": "uuid-1", "serviceId": "uuid", "priceOverride": null, "durationMinutesOverride": null },
{ "staffId": "uuid-2", "serviceId": "uuid", "priceOverride": null, "durationMinutesOverride": null }
]
}
Returns the assignment row for one staff member on one service, including current price and duration overrides. Staff callers can only fetch their own row. For the reverse lookup — all services a given staff member is assigned to — see GET /query/booking/staff/{staffId}/services in the Staff section.
{
"result": "success",
"staffService": {
"staffId": "uuid",
"serviceId": "uuid",
"priceOverride": "49.99",
"durationMinutesOverride": 30
}
}
Sets or clears per-staff price and duration overrides for a service assignment. Requires merchant allowStaffPricingOverrides = true. Staff callers must have canManageOwnPricing = true and can only patch their own row. Send null to clear an override.
| Field | Type | Description |
|---|---|---|
priceOverride | number | null | Override price for this staff member. null clears it. |
durationMinutesOverride | integer | null | Override duration in minutes. null clears it. |
{
"staffService": {
"priceOverride": 49.99,
"durationMinutesOverride": 45
}
}
Manage staff members under the merchant, their schedules, recurring breaks, and one-off date exceptions.
Creates a new staff member. Merchant only.
| Field | Type | Required | Description |
|---|---|---|---|
firstName | string | Yes | 1–100 characters. |
lastName | string | Yes | 1–100 characters. |
email | string | No | Valid email address. |
userExternalId | integer | No | Gateway user ID. Links this staff record to a gateway user account so the user can authenticate as staff. |
color | string | No | Hex color for UI display. Defaults to #6083b4. |
isActive | boolean | No | Whether the staff member is active. Defaults to true. Inactive staff cannot be assigned to new bookings. |
canManageSchedule | boolean | No | Staff can edit their own schedule. |
canManageBreaks | boolean | No | Staff can edit their own breaks. |
canManageExceptions | boolean | No | Staff can edit their own date exceptions. |
canCreateService | boolean | No | Staff can create services. |
canEditService | boolean | No | Staff can edit assigned services. |
canDeleteService | boolean | No | Staff can delete assigned services. |
canViewAllServices | boolean | No | Staff can view all services; otherwise only assigned ones. |
canViewAllBookings | boolean | No | Staff can view all bookings; otherwise only their own. |
canCreateBooking | boolean | No | Staff can create bookings on behalf of customers. |
canManageBooking | boolean | No | Staff can update booking status and notes. |
canManageOwnPricing | boolean | No | Staff can set their own price/duration overrides (requires merchant allowStaffPricingOverrides). |
{
"staff": {
"firstName": "Jane",
"lastName": "Smith",
"email": "jane@example.com",
"userExternalId": 101,
"color": "#FF5733",
"canCreateBooking": true,
"canManageBooking": true,
"canViewAllBookings": true
}
}
Returns all staff under the merchant. Merchant tokens receive full records; staff tokens receive only { id, firstName, lastName, email, color } per entry. Also accessible with public permission via ?is_public=true — public callers receive each entry reduced further to { id, firstName, lastName, color } (no email).
Returns a staff member. Merchant tokens get the full record including all permission flags. Staff tokens get a slim record with { id, firstName, lastName, email, color } only. Also accessible with public permission via ?is_public=true — public callers receive { id, firstName, lastName, color } only (no email, no sub-resources, no permission flags).
Updates a staff record. Merchants can update all fields including permission flags and isActive. Staff can only update their own identity fields (firstName, lastName, email, color).
| Field | Type | Description |
|---|---|---|
firstName | string | 1–100 characters. |
lastName | string | 1–100 characters. |
email | string | Valid email address. |
userExternalId | integer | Positive integer. |
color | string | Valid hex color. |
isActive | boolean | Set to false to deactivate the staff member. Inactive staff cannot be assigned to new bookings. |
canManageSchedule | boolean | |
canManageBreaks | boolean | |
canManageExceptions | boolean | |
canCreateService | boolean | |
canEditService | boolean | |
canDeleteService | boolean | |
canViewAllServices | boolean | |
canViewAllBookings | boolean | |
canCreateBooking | boolean | |
canManageBooking | boolean | |
canManageOwnPricing | boolean |
Staff callers can only update firstName, lastName, email, and color. Any other field results in 422.
{
"staff": {
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@example.com",
"color": "#FF5733",
"isActive": true,
"canManageSchedule": true,
"canManageBreaks": true,
"canCreateBooking": true,
"canManageBooking": true,
"canViewAllBookings": false,
"canCreateService": false,
"canEditService": false,
"canDeleteService": false,
"canViewAllServices": false,
"canManageOwnPricing": false
}
}
{
"staff": {
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@example.com",
"color": "#FF5733"
}
}
Permanently deletes a staff member and all their sub-resources (schedules, breaks, exceptions). Merchant only.
Returns the services this staff member is assigned to, via the staff/service join. Each entry is a full service record plus that staff member's priceOverride and durationMinutesOverride (both null if unset). Returns an empty array if the staff member has no assignments. This is the reverse lookup of GET /query/booking/services/{serviceId}/staff/{staffId}. Staff tokens may only read their own assigned services.
{
"result": "success",
"services": [
{
"id": "uuid",
"merchantId": "uuid",
"name": "Haircut",
"price": "50",
"durationMinutes": 30,
"priceOverride": null,
"durationMinutesOverride": null
}
]
}
Returns the staff member's weekly work schedule. Returns an empty array if no schedule has been set yet. Staff tokens may only read their own schedule.
{
"result": "success",
"schedule": [
{ "id": "uuid", "staffId": "uuid", "dayOfWeek": 0, "isActive": false, "startTime": "", "endTime": "" },
{ "id": "uuid", "staffId": "uuid", "dayOfWeek": 1, "isActive": true, "startTime": "09:00", "endTime": "17:00" }
]
}
Replaces the staff member's full weekly work schedule. Must send all 7 days. Staff require canManageSchedule = true.
| Field | Type | Required | Description |
|---|---|---|---|
dayOfWeek | integer 0–6 | Yes | 0 = Sunday, 6 = Saturday. |
isActive | boolean | Yes | Whether the staff member works that day. |
startTime | string HH:MM | Conditional | Required when isActive is true. |
endTime | string HH:MM | Conditional | Required when isActive is true. Must be after startTime. |
{
"schedule": [
{ "dayOfWeek": 0, "isActive": false },
{ "dayOfWeek": 1, "isActive": true, "startTime": "09:00", "endTime": "17:00" },
{ "dayOfWeek": 2, "isActive": true, "startTime": "09:00", "endTime": "17:00" },
{ "dayOfWeek": 3, "isActive": true, "startTime": "09:00", "endTime": "17:00" },
{ "dayOfWeek": 4, "isActive": true, "startTime": "09:00", "endTime": "17:00" },
{ "dayOfWeek": 5, "isActive": true, "startTime": "09:00", "endTime": "13:00" },
{ "dayOfWeek": 6, "isActive": false }
]
}
Returns all recurring breaks for the staff member. Returns an empty array if none have been created. Staff tokens may only read their own breaks.
{
"result": "success",
"breaks": [
{ "id": "uuid", "staffId": "uuid", "dayOfWeek": 1, "startTime": "12:00", "endTime": "13:00", "isActive": true }
]
}
Returns a single break by ID. The break must belong to the specified staff member. Staff tokens may only read their own breaks.
Adds a recurring break window to a specific day of the week. Staff require canManageBreaks = true.
| Field | Type | Required | Description |
|---|---|---|---|
dayOfWeek | integer 0–6 | Yes | Day the break applies to. |
startTime | string HH:MM | Yes | Break start. Must be before endTime. |
endTime | string HH:MM | Yes | Break end. Must be after startTime. |
isActive | boolean | No | Defaults to true. |
Updates one or more fields on an existing break. Staff require canManageBreaks = true. All fields optional. When only one of startTime or endTime is sent, the other is read from the database to validate ordering.
| Field | Type | Description |
|---|---|---|
dayOfWeek | integer 0–6 | Day the break applies to. |
startTime | string HH:MM | Break start time. |
endTime | string HH:MM | Break end time. Must be after startTime. |
isActive | boolean | Enable or disable this break without deleting it. |
{
"break": {
"dayOfWeek": 1,
"startTime": "12:00",
"endTime": "13:00",
"isActive": true
}
}
Removes a recurring break from a staff member. Staff require canManageBreaks = true.
Returns all date exceptions for the staff member. Returns an empty array if none have been created. Staff tokens may only read their own exceptions.
{
"result": "success",
"exceptions": [
{ "id": "uuid", "staffId": "uuid", "date": "2026-12-25", "isAvailable": false, "startTime": null, "endTime": null, "breaks": [] }
]
}
Returns a single date exception by ID. The exception must belong to the specified staff member. Staff tokens may only read their own exceptions.
Adds or updates a one-off availability override for a staff member (day off or special hours). Idempotent on the same date — calling again with the same date updates the existing record, including replacing its breaks (see below). Staff require canManageExceptions = true.
An exception may also carry breaks: non-repeating break windows scoped to that single date. Exception breaks are additive to the staff member's recurring breaks for that day of week — creating or updating an exception to change hours/availability does not drop the recurring breaks that would otherwise apply. Use breaks when a specific date needs an extra break on top of the usual ones (e.g. a one-off appointment). breaks is only allowed when isAvailable is true, and each PUT fully replaces the exception's existing breaks — omit the field to clear them.
| Field | Type | Required | Description |
|---|---|---|---|
date | string YYYY-MM-DD | Yes | The date to override. |
isAvailable | boolean | Yes | false = day off. true = working special hours. |
startTime | string HH:MM | Conditional | Required when isAvailable is true. |
endTime | string HH:MM | Conditional | Required when isAvailable is true. Must be after startTime. |
breaks | array | No | Only allowed when isAvailable is true. Fully replaces the exception's existing breaks. |
breaks[].startTime | string HH:MM | Yes (per entry) | Must be before endTime. |
breaks[].endTime | string HH:MM | Yes (per entry) | Must be after startTime. |
breaks[].isActive | boolean | No | Defaults to true. |
// Day off
{ "exceptions": { "date": "2026-08-15", "isAvailable": false } }
// Special hours
{ "exceptions": { "date": "2026-08-20", "isAvailable": true, "startTime": "10:00", "endTime": "14:00" } }
// Special hours with an extra one-off break
{
"exceptions": {
"date": "2026-08-22",
"isAvailable": true,
"startTime": "09:00",
"endTime": "17:00",
"breaks": [
{ "startTime": "12:00", "endTime": "13:00" }
]
}
}
Removes a one-off date exception for a staff member. Staff require canManageExceptions = true.
manage flows require an active gateway session, a Bearer token containing a valid sub / user_id claim, or the merchant's Basic Auth credential.slots and POST /query/booking accept Basic Auth alone and run with public permission for slots, or public permission for booking-create only when is_public_booking: true is set (otherwise full merchant permission).GET /query/booking/staff, GET /query/booking/staff/{staffId}, GET /query/booking/services, and GET /query/booking/merchant also run with public permission when called with ?is_public=true; each returns a narrower field set for public callers than for authenticated merchant/staff callers.manage flows use the signed manage token returned at booking creation as their sole credential. Do not expose manage tokens unnecessarily.false by default. Merchants must explicitly grant each capability when creating or updating a staff record.allowStaffPricingOverrides = true and the service uses providerMode = atCreation.capacityMode = perStaff gives each staff member independent capacity for a slot (unassigned bookings count conservatively against every staff member's capacity until assigned). capacityMode = perService shares capacity across all staff for the slot — once a staff member has an active booking there, only that staff member can take further bookings and the slot is hidden from other staff.Manage organizations and look up the merchants that belong to one.
Authentication is different from other endpoints: organizations are scoped by user (the owner and any linked team members), not by merchant. Every call below still authenticates the same way as any other endpoint — either a Bearer token or x_login/x_tran_key — but the gateway resolves it to a user:
Authorization: Bearer <token> (or token field). The user is read directly from the token's sub claim.x_login / x_tran_key — validated as merchant credentials exactly like every other endpoint, then resolved to that merchant's attached user account. That resolved user is who the organization actions run as.A user can only read or manage organizations they own or belong to. There is no unauthenticated/public route on this endpoint.
{
"uniqueID": 4,
"name": "Acme Salons",
"orgKey": "acme-salons",
"ownerUserId": 12,
"notes": "Rolled up under the Acme parent account",
"enabled": true
}
This shape is returned under organization for create/read/update, and under organizations (as a list) for GET /query/organization.
Returns the organizations the authenticated user owns or belongs to. If none exist, returns an empty list: { "organizations": [] } with status 200.
Optional query parameters for GET /query/organization:
page: page number (1-based). When provided, results are paginated.pageSize: records per page. Optional; defaults to 50.Returns 404 if the organization doesn't exist, is disabled, or the authenticated user doesn't have access to it.
The authenticated user becomes the organization's owner. System administrator accounts cannot create organizations.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Organization name (minimum 2 characters). |
org_key | string | No | Public slug used to look up the organization (e.g. via GET /query/organization/merchants). 2-64 letters, numbers, dashes, or underscores; must be unique. |
notes | string | No | Free-form notes. |
{
"x_login": "...",
"x_tran_key": "...",
"organization": {
"name": "Acme Salons",
"org_key": "acme-salons",
"notes": "Rolled up under the Acme parent account"
}
}
Only the organization's owner or an admin member may update it. Omit fields you don't want to change.
| Field | Type | Required | Description |
|---|---|---|---|
name | string | No | Organization name (minimum 2 characters). |
org_key | string | No | Public slug; must remain unique. Send an empty string to clear it. |
notes | string | No | Free-form notes. |
{
"x_login": "...",
"x_tran_key": "...",
"organization": {
"name": "Acme Salons & Spa",
"notes": "Updated after the spa line launch"
}
}
Soft-disables the organization (enabled = false) rather than removing the row, and unlinks every merchant that belonged to it (so they're free to join another organization). Once disabled, it stops appearing in list/lookup results for every user, including the owner. Only the organization's owner or an admin member may delete it.
Looks up an organization by its public org_key and returns the merchant IDs linked to it. Requires the same authentication as every other action above — there is no public/unauthenticated version of this route.
| Field | Type | Required | Description |
|---|---|---|---|
org_key | string | Yes | The organization's public slug. |
GET /query/organization/merchants?org_key=acme-salons
{
"result": "success",
"merchants": [
{ "merchant_id": 1024 },
{ "merchant_id": 1031 }
]
}
Issue and manage cards via the central query handler. Primary-account endpoints require merchant authentication; the cardholder list-cards endpoint uses cardholder credentials in the body.
Two request sections:
Optional filters: search, type (virtual|physical), status.
GET /query/expicard
Returns full details (including PAN/CVC) for authorized merchant/enhanced users. The response includes card.id (the requested card ID) and card.token (partner token).
GET /query/expicard?uniqueID=CARD-12345
Lightweight, cached card summary without sensitive details; falls back to full read if unavailable. Includes card.id (requested card ID) and card.token (partner token).
GET /query/expicard?subaction=summary&uniqueID=CARD-12345
Body fields: first_name, last_name, email, amount, address_line1, city, state, zip, type (virtual|physical), cardholder_type (team_member|vendor|contractor), optional external_id.
{
"action": "expicard",
"subaction": "create",
"first_name": "Jane",
"last_name": "Doe",
"email": "jane@example.com",
"password": "TempPassword123",
"amount": 200.00,
"address_line1": "123 Main St",
"city": "Austin",
"state": "TX",
"zip": "78701",
"type": "virtual",
"cardholder_type": "team_member",
"external_id": "CARD-EXT-001"
}
{
"action": "expicard",
"subaction": "fund",
"uniqueID": "CARD-67890",
"amount": 50.00
}
Optional filters supported: from, to, status.
GET /query/expicard?subaction=transactions&uniqueID=CARD-12345&from=2025-10-01&to=2025-10-31&status=approved
Creates a funding bank (bank account) for a specific subaccount.
Body fields: subaccountId, bankAccountNumber, bankRoutingNumber. Additional fields may be included per partner requirements.
Note: The query handler lowercases top-level JSON keys. To avoid casing issues, you can send snake_case keys (bank_account_number, bank_routing_number) or nest fields under a bank object.
{
"action": "expicard",
"subaction": "subaccount_bank_create",
"subaccountId": "SUB-123456",
"bank_account_number": "1234567890",
"bank_routing_number": "021000021"
}
Note: Sensitive card detail fields are only returned to authorized merchant/enhanced users; summaries are optimized for portal views.
These endpoints do not require merchant authentication. They use merchant API credentials for partner access internally, but require cardholder username and password in the request body.
Response: Returns full card details for the authenticated cardholder, including sensitive fields such as full card number (PAN), CVC, and expiration. Handle this response securely.
Authentication: You can provide the cardholder username/password either in the body or via HTTP Basic Auth (recommended). When using Basic Auth, the body credentials are optional.
Authorization: Basic <base64(cardholder_username:cardholder_password)>
{
"action": "expicard",
"role": "user",
"subaction": "user_cards",
"username": "cardholder@example.com",
"password": "cardholderPassword"
}
Note: User endpoints restrict access to cards linked to the authenticated cardholder; sensitive fields are not returned.