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/mfaresend — Starts a replacement challenge for the method in mfa_token and returns a replacement token.POST /query/auth/mfarecovery — Requires an SMS mfa_token plus password re-entry and starts controlled email recovery.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_method": "sms"
}
{
"mfa_token": "sms_mfa_token",
"password": "your_password"
}
Email recovery never disables SMS and is limited to one successful login per rolling 24 hours.
{
"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 or wallet. |
payment_method_id | number | Conditional | Existing saved payment method id (CustomerDetails). Required unless charging with token or wallet. Aliases: x_payment_id, paymentmethodid. |
wallet | object | Conditional | Digital wallet charge. Required unless charging with token or payment_method_id. Shape: {"type": "apple_pay", "payment_data": {...}}, where payment_data is the PKPaymentToken.paymentData object from Apple Pay. Single-use; cannot be combined with save_payment_method. |
cvc | string | Conditional | Required for token-based charges, and accepted for payment_method_id charges. Provide at charge time (do not store). You can also pass billing.cvc. Not applicable to wallet charges. |
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). Not allowed for wallet charges. |
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,
"cvc": "123",
"transtype": "AUTH_CAPTURE"
}
{
"wallet": {
"type": "apple_pay",
"payment_data": {
"version": "EC_v1",
"data": "...",
"signature": "...",
"header": {
"ephemeralPublicKey": "...",
"publicKeyHash": "...",
"transactionId": "..."
}
}
},
"amount": 12.34,
"billing": {
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com"
}
}
{
"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,
"payment_method": "card",
"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/wallet; invalid or expired token; invalid card number, expiration, or CVC; missing cvc on a token-based charge; save_payment_method without a token or with a wallet charge; unsupported wallet.type or missing wallet.payment_data; 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 one of token, payment_method_id, or wallet.cvc (or billing.cvc). CVC is not stored in tokens.payment_method_id charges accept cvc too. It is optional here, but processors configured to require a CVC will decline the charge without one, so send it whenever you have it.payment_method_id, customer_id is optional but (if provided) must match.save_payment_method requires a token-based charge and is not supported for wallet charges (wallet payment data is single-use).wallet charges currently support Apple Pay only, and Apple Pay must be enabled for the merchant account before a wallet charge can be processed.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, which is null when no customer is associated.
Customer association is optional. On create, the API creates a customer only when a customer object or populated billing/shipping fields are provided. On update, send customer_id: null to detach the customer.
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_subtotal | number | No | Amount before tax, surcharge, and tip. If omitted, inferred as amount_total - amount_surcharge - amount_tax - amount_tip. |
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 | Ignored on create. The transaction timestamp is generated from the database clock in YYYY-MM-DD HH:MM:SS format. 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|null | No | Optional customer association. Omit or set to null to create an unassociated transaction; on update, null detaches the current customer. A customer is created only when customer or populated billing/shipping data is provided. |
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_subtotal": 10.09,
"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}
]
}
{
"amount_total": 12.34,
"amount_subtotal": 12.34,
"description": "Guest checkout record",
"customer_id": null
}
PUT /query/transaction/{id}
{
"customer_id": null
}
{
"result": "success",
"transaction": {
"uniqueID": 123456,
"customer_id": 555,
"coupon_id": 0,
"recurring_id": 0,
"amount_total": 12.34,
"amount_subtotal": 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"
}
}
Apple Pay lets customers authorize a card payment from a supported Apple device. You can offer it through a hosted checkout page or submit an Apple Pay payment token through POST /charges from your own checkout.
| Integration | What you need to do |
|---|---|
| Hosted checkout | Request Apple Pay enablement. The Apple Pay button is displayed automatically to eligible customers on supported invoice and hosted payment pages. No Apple certificates or JavaScript integration are required from you. |
| Your website | Request enablement and provide every domain or subdomain where the Apple Pay button will appear. Complete domain verification as described below, then use Apple Pay JS to collect a payment token and submit it to POST /charges. |
| Your app | Request enablement, configure the Apple Pay capability in your app, and send the resulting PKPaymentToken.paymentData object to POST /charges. |
https://<your-domain>/.well-known/apple-developer-merchantid-domain-association. It must be publicly available over HTTPS without authentication or a redirect.Hosted checkout pages handle merchant validation automatically. When Apple Pay JS fires onvalidatemerchant, the page sends the event's validationURL with a short-lived validation token issued by the gateway. The merchant account ID is derived from that signed token and must not be supplied by browser code.
session.onvalidatemerchant = async (event) => {
const response = await fetch('/applepay/validate-merchant', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
validationURL: event.validationURL,
validationToken: checkout.applePayValidationToken
})
});
if (!response.ok) {
session.abort();
return;
}
const merchantSession = await response.json();
session.completeMerchantValidation(merchantSession);
};
Always use the validationURL supplied by the Apple Pay event. Never expose gateway credentials or construct a validation token in browser code. A merchant session is short-lived and single-use; do not cache or reuse it.
After the customer authorizes the payment, send event.payment.token.paymentData unchanged as wallet.payment_data. The charge amount must match the amount shown in the Apple Pay payment sheet.
{
"wallet": {
"type": "apple_pay",
"payment_data": {
"version": "EC_v1",
"data": "...",
"signature": "...",
"header": {
"ephemeralPublicKey": "...",
"publicKeyHash": "...",
"transactionId": "..."
}
}
},
"amount": 12.34,
"billing": {
"first_name": "Jane",
"last_name": "Smith",
"email": "jane@example.com"
}
}
See the Charges documentation for authentication, idempotency, the complete request schema, and response fields.
ApplePaySession.canMakePayments() returns true.paymentData as sensitive, single-use payment data. Send it only to the gateway over HTTPS; do not log, alter, decrypt, cache, or reuse it.save_payment_method and cannot be initiated from a virtual terminal.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./query/disputes API is deprecated and no longer maintained. Use the Evidence API's Disputes endpoints instead — new integrations should not build against this one, and existing integrations should migrate when possible.
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.Process Apple Tap to Pay on iPhone transactions through Query. The mobile client sends encrypted ttp_… envelopes; Expitrans decrypts them only to forward the required provider fields. Never decrypt, alter, or log those envelopes in your application.
Authentication: Authorization: Bearer <access_token>, Basic merchant credentials, or x_login and x_tran_key in the request body.
POST with a JSON body. Documented field names are case-insensitive. Successful provider responses are returned as magensaResponse; a locally persisted transaction is included as transaction when available.magensaResponse is Magensa's response object and only contains fields supplied by the processor. The client model exposes transactionOutput.authorizedAmount as a string; Magensa can serialize it as either a JSON string or number.
Exchange the identifier returned by Apple's reader APIs for the token used to configure the payment card reader.
| Field | Type | Required | Description |
|---|---|---|---|
paymentCardReaderIdentifier | string | Yes | Apple payment-card reader identifier, at least six characters. |
{ "paymentCardReaderIdentifier": "reader_01HZXT8C3MGK" }{
"result": "success",
"paymentCardReader": {
"traceID": "provider-trace-id",
"customerTransactionID": "reader-token-request-id",
"transactionUTCTimeStamp": "2026-07-28T16:30:00Z",
"paymentCardReaderToken": "<reader-token>"
}
}customerTransactionID is the sale's stable idempotency key. Reusing it with the same amount after the first attempt returns 409; reusing it with different transaction data returns 422. Treat either result as a reconciliation signal, not a reason to issue a new sale.
| Field | Type | Required | Description |
|---|---|---|---|
customerTransactionID | string | Yes | Unique client-generated sale ID, 1–128 characters. |
transactionInput.transactionType | string | Yes | Must be SALE. |
transactionInput.amount | number | Yes | Positive amount as a JSON number. |
transactionContext.customer_id | integer | Yes | Existing Expi customer ID for local transaction history. |
transactionContext.description | string | No | Local transaction description. |
transactionContext.amount_subtotal | number | No | Subtotal saved in local history. If omitted, it is inferred from the authorized total minus tax, surcharge, and tip. |
transactionContext.amount_tax | number | No | Tax amount saved in local history. |
transactionContext.amount_surcharge | number | No | Surcharge amount saved in local history. |
dataInput.encryptedData.dataType | string | Yes | Must be AppleTapToPay. |
dataInput.encryptedData.data | string | Yes | Encrypted Apple payment data in a ttp_… envelope. |
dataInput.tlvList | string | Yes | Encrypted EMV TLV data in a ttp_… envelope. |
dataInput.paymentMode | string | Yes | Must be EMV. |
dataInput.paymentType | string | Yes | Must be CREDIT. |
deviceInfo | object | No | When present, all four fields below are required. |
deviceInfo.serialNumber, make, model, nickName | string | Conditional | Reader metadata forwarded to the provider when deviceInfo is supplied. |
{
"customerTransactionID": "ttp-sale-01HZXT8C3MGK",
"transactionInput": { "transactionType": "SALE", "amount": 12.34 },
"transactionContext": { "customer_id": 12345, "description": "Coffee and pastry" },
"dataInput": {
"encryptedData": { "dataType": "AppleTapToPay", "data": "ttp_<encrypted-apple-payment-data>" },
"tlvList": "ttp_<encrypted-emv-tlv-data>",
"paymentMode": "EMV",
"paymentType": "CREDIT"
},
"deviceInfo": { "serialNumber": "D123456", "make": "Apple", "model": "iPhone", "nickName": "Front counter" }
}{
"result": "success",
"magensaResponse": {
"traceID": "provider-trace-id",
"magTranID": "mag-transaction-id",
"customerTransactionID": "ttp-sale-01HZXT8C3MGK",
"transactionUTCTimeStamp": "2026-07-28T16:30:00Z",
"transactionOutput": { "isTransactionApproved": true, "authorizedAmount": "12.34", "authCode": "A1B2C3", "transactionID": "processor-transaction-id", "transactionStatus": "APPROVED", "transactionMessage": "Approved" },
"dataOutput": { "panLast4": "4242" }
},
"transaction": { "uniqueID": 98765, "presentation_id": 98765 }
}Keep magTranID for post-sale operations and traceID for provider support. A successful HTTP response is not itself an approval—always check transactionOutput.isTransactionApproved.
Other optional provider fields include additionalResponseData, dataOutput.additionalOutputData, dataOutput.cardID, and the converted or normalized processor responses. Treat card IDs, tokens, issuer data, scripts, and receipts as sensitive.
Use the original sale's magTranID. The reference must identify an approved Tap to Pay sale recorded for the authenticated merchant. transactionInputDetails must contain the Magensa fields required for the original payment; this API validates only that it is an object and forwards it unchanged.
| Field | Type | Required | Description |
|---|---|---|---|
referenceMagTranID | string | Yes | magTranID from the original Tap to Pay sale. |
tipAmount | number | Yes | New tip amount; zero is valid. |
transactionInputDetails | object | Yes | Provider-required original transaction detail fields. |
{
"referenceMagTranID": "mag-transaction-id",
"tipAmount": 2.50,
"transactionInputDetails": { "amount": 12.34 }
}{
"result": "success",
"magensaResponse": { "traceID": "provider-trace-id", "magTranID": "mag-tip-adjustment-id", "transactionUTCTimeStamp": "2026-07-28T16:30:00Z", "transactionOutput": { "isTransactionApproved": true, "transactionStatus": "APPROVED", "transactionMessage": "Approved" } },
"transaction": { "uniqueID": 98765, "tip": 2.50 }
}Use the original sale's magTranID. This operation is idempotent by referenceMagTranID: retrying the same reference returns 409, and changing its amount returns 422. Include a positive amount only for a partial void; omit it to use the original amount.
| Field | Type | Required | Description |
|---|---|---|---|
referenceMagTranID | string | Yes | magTranID from the original approved Tap to Pay sale. |
amount | number | No | Positive partial or full operation amount. |
{
"referenceMagTranID": "mag-transaction-id",
"amount": 12.34
}{
"result": "success",
"magensaResponse": { "traceID": "provider-trace-id", "magTranID": "mag-void-id", "transactionUTCTimeStamp": "2026-07-28T16:30:00Z", "transactionOutput": { "isTransactionApproved": true, "transactionStatus": "APPROVED", "transactionMessage": "Approved" } },
"transaction": { "uniqueID": 98766, "reference_id": 98765 }
}Use the original sale's magTranID. This operation is idempotent by referenceMagTranID: retrying the same reference returns 409, and changing its amount returns 422. Include a positive amount only for a partial refund; omit it to use the original amount.
| Field | Type | Required | Description |
|---|---|---|---|
referenceMagTranID | string | Yes | magTranID from the original approved Tap to Pay sale. |
amount | number | No | Positive partial or full refund amount. |
{
"referenceMagTranID": "mag-transaction-id",
"amount": 12.34
}{
"result": "success",
"magensaResponse": { "traceID": "provider-trace-id", "magTranID": "mag-refund-id", "transactionUTCTimeStamp": "2026-07-28T16:30:00Z", "transactionOutput": { "isTransactionApproved": true, "authorizedAmount": "12.34", "transactionStatus": "APPROVED", "transactionMessage": "Approved" } },
"transaction": { "uniqueID": 98767, "reference_id": 98765 }
}Invalid envelopes, missing fields, and unsupported values return the standard failed response. Provider failures include a structured providerError when Magensa returns one.
{
"result": "failed",
"message": "Provider declined the transaction",
"providerError": { "code": "DECLINED", "message": "Provider declined the transaction", "traceID": "provider-trace-id" }
}POST is supported; other methods return 405. Invalid operation paths return 404.2xx provider response can still have isTransactionApproved: false; always inspect that flag.transaction data is supplemental. magensaResponse remains the provider's authoritative outcome.Process encrypted MagTek physical-card-reader transactions through Query. These routes use the merchant's server-side Magensa credentials; send the encrypted reader output only. They do not accept Apple Tap to Pay ttp_… envelopes.
Authentication: Authorization: Bearer <access_token>, Basic merchant credentials, or x_login and x_tran_key in the request body.
POST and JSON. Field names are case-insensitive, but the camel-case names below are canonical. Provider results are returned under magensaResponse.magensaResponse is Magensa's response object and only contains fields supplied by the processor. The client model exposes transactionOutput.authorizedAmount as a string; Magensa can serialize it as either a JSON string or number.
Checks the configured Magensa physical-reader credentials. There are no operation-specific request fields; when using body credentials, include the authentication fields described above.
{}{
"result": "success",
"magensaResponse": {
"code": "404",
"message": "Transaction not found"
}
}Magensa documents the provider's 404 for its credential-check transaction (custTranID=0) as a successful authorization check, so it is returned in the success envelope above.
customerTransactionID is the sale idempotency key. Do not reuse it for another sale. If the network result is unknown, call recall before attempting any recovery. The reader ARQC must be an even-length hexadecimal string; it is normalized to uppercase before forwarding.
| Field | Type | Required | Description |
|---|---|---|---|
customerTransactionID | string | Yes | Client-generated sale ID, 1–128 characters. Reuse only when referring to this exact sale. |
transactionInput.transactionType | string | Yes | Must be SALE. |
transactionInput.amount | number | Yes | Positive sale amount. Send a JSON number, not a quoted value. |
transactionContext.customer_id | integer | No | Existing Expi customer ID for local transaction history. Omit it, send null, or send a non-positive integer for a walk-in sale; Expitrans uses or creates this merchant's active Walk-In Customer. |
transactionContext.description | string | No | Local transaction description. |
transactionContext.amount_subtotal | number | No | Subtotal saved in local history. If omitted, it is inferred from the authorized total minus tax, surcharge, and tip. |
transactionContext.amount_tax | number | No | Tax amount saved in local history. |
transactionContext.amount_surcharge | number | No | Surcharge amount saved in local history. |
dataInput.encryptedData.dataType | string | Yes | Must be ARQC. |
dataInput.encryptedData.data | string | Yes | Even-length hexadecimal encrypted reader output. Never decrypt or log it. |
dataInput.paymentType | string | Yes | Must be Credit. |
{
"customerTransactionID": "external-sale-01HZXT8C3MGK",
"transactionInput": { "transactionType": "SALE", "amount": 12.34 },
"transactionContext": { "description": "Coffee and pastry" },
"dataInput": {
"encryptedData": { "dataType": "ARQC", "data": "A1B2C3D4E5F6" },
"paymentType": "Credit"
}
}{
"result": "success",
"magensaResponse": {
"traceID": "provider-trace-id",
"magTranID": "mag-transaction-id",
"customerTransactionID": "external-sale-01HZXT8C3MGK",
"transactionUTCTimeStamp": "2026-07-28T16:30:00Z",
"transactionOutput": {
"isTransactionApproved": true,
"authorizedAmount": "12.34",
"authCode": "A1B2C3",
"transactionID": "processor-transaction-id",
"transactionStatus": "APPROVED",
"transactionMessage": "Approved",
"issuerAuthenticationData": "<return-to-reader-when-present>"
},
"dataOutput": { "panLast4": "4242" }
},
"transaction": { "uniqueID": 98765, "presentation_id": 98765 }
}For chip flows, return issuerAuthenticationData (and either issuer script template when supplied) to the reader according to the reader integration. Retain magTranID for all post-sale operations and traceID for provider support.
Other optional provider fields include additionalResponseData, dataOutput.additionalOutputData, dataOutput.cardID, and the converted or normalized processor responses. Treat card IDs, tokens, issuer data, scripts, and receipts as sensitive.
Use this after a timeout or disconnected client to retrieve the provider outcome for the original customerTransactionID. It does not submit another charge.
| Field | Type | Required | Description |
|---|---|---|---|
customerTransactionID | string | Yes | The original EMV sale's customer transaction ID. |
{ "customerTransactionID": "external-sale-01HZXT8C3MGK" }{
"result": "success",
"magensaResponse": {
"traceID": "provider-trace-id",
"magTranID": "mag-transaction-id",
"customerTransactionID": "external-sale-01HZXT8C3MGK",
"transactionUTCTimeStamp": "2026-07-28T16:30:00Z",
"transactionOutput": { "isTransactionApproved": true, "authorizedAmount": "12.34", "transactionStatus": "APPROVED", "transactionMessage": "Approved" }
}
}Use the sale response's magTranID as referenceMagTranID. transactionInputDetails must contain the Magensa fields required for the original payment; this API validates only that it is an object and forwards it unchanged.
| Field | Type | Required | Description |
|---|---|---|---|
referenceMagTranID | string | Yes | magTranID from an approved External Reader sale owned by this merchant. |
tipAmount | number | Yes | New tip amount; zero is valid. |
transactionInputDetails | object | Yes | Provider-required transaction detail fields for the original payment. |
{
"referenceMagTranID": "mag-transaction-id",
"tipAmount": 2.50,
"transactionInputDetails": { "amount": 12.34 }
}{
"result": "success",
"magensaResponse": { "traceID": "provider-trace-id", "magTranID": "mag-tip-adjustment-id", "transactionUTCTimeStamp": "2026-07-28T16:30:00Z", "transactionOutput": { "isTransactionApproved": true, "transactionStatus": "APPROVED", "transactionMessage": "Approved" } },
"transaction": { "uniqueID": 98765, "tip": 2.50 }
}Use the original sale's magTranID. Only an approved, not-yet-voided External Reader sale can be voided. A declined, unapproved, or already-voided sale returns 422 without sending a request to Magensa. If the gateway cannot verify the sale's local void history, it returns 503 and does not submit the void. Include a positive amount for a partial void; omit it when Magensa should use the original transaction amount. A concurrent repeat may return 409; a repeat after a successful void returns 422.
| Field | Type | Required | Description |
|---|---|---|---|
referenceMagTranID | string | Yes | magTranID from an approved External Reader sale owned by this merchant. |
amount | number | No | Positive partial or full operation amount. |
{
"referenceMagTranID": "mag-transaction-id",
"amount": 12.34
}{
"result": "success",
"magensaResponse": { "traceID": "provider-trace-id", "magTranID": "mag-void-id", "transactionUTCTimeStamp": "2026-07-28T16:30:00Z", "transactionOutput": { "isTransactionApproved": true, "transactionStatus": "APPROVED", "transactionMessage": "Approved" } },
"transaction": { "uniqueID": 98766, "reference_id": 98765 }
}Use the original sale's magTranID and a new customerTransactionID for this refund. The refund ID is its idempotency key; reuse it only to retry this exact refund. Include a positive amount for a partial refund, or omit it to use the original transaction amount.
| Field | Type | Required | Description |
|---|---|---|---|
referenceMagTranID | string | Yes | magTranID from an approved External Reader sale owned by this merchant. |
customerTransactionID | string | Yes | New 1–128 character refund ID. Reuse only to retry this exact refund. |
amount | number | No | Positive partial or full refund amount. |
{
"referenceMagTranID": "mag-transaction-id",
"customerTransactionID": "external-refund-01HZXT8C3MGK",
"amount": 12.34
}{
"result": "success",
"magensaResponse": { "traceID": "provider-trace-id", "magTranID": "mag-refund-id", "transactionUTCTimeStamp": "2026-07-28T16:30:00Z", "transactionOutput": { "isTransactionApproved": true, "authorizedAmount": "12.34", "transactionStatus": "APPROVED", "transactionMessage": "Approved" } },
"transaction": { "uniqueID": 98767, "reference_id": 98765 }
}A provider rejection or transport error uses the standard failure envelope. The provider's HTTP status is preserved where possible.
{
"result": "failed",
"message": "Provider declined the transaction",
"providerError": { "code": "DECLINED", "message": "Provider declined the transaction", "traceID": "provider-trace-id" }
}409 or 422; do not retry with a new ID until the original outcome is known.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. |
recurring.paused | A recurring schedule is automatically paused after 3 consecutive failed payment attempts. |
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"
}
}
}
Deposits (payouts) made to your account. Payouts are pulled from the same backend as the Payouts page of the dashboard, so only merchants with a MID configured on their account can use this endpoint. List-only: there is no single-payout lookup by id, and payouts have no accept/challenge-style response workflow.
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: deposit_date, amount. Defaults to -deposit_date. |
filters | string | No | Standard filter expression (e.g. filters=amount>=100). Applied to the current page only, since payouts 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",
"payouts": [
{
"uniqueID": 91234,
"deposit_date": "2026-08-01",
"amount": 1542.30,
"routing_number": "****6789"
}
],
"pageCount": 3,
"totalCount": 58
}
Fields are included only when the upstream system provides them.
| Field | Type | Description |
|---|---|---|
uniqueID | number | Payout identifier. |
deposit_date | string | Date the deposit was made. |
amount | number | Deposit amount. |
routing_number | string | Masked to the last 4 digits, matching what the dashboard's Payouts page shows. |
| Code | Meaning | When it is returned |
|---|---|---|
200 | OK | The payout list was returned successfully. |
400 | Bad Request | Invalid page/pageSize; unsupported sort field; or a request for a single payout by id (not supported). |
401 | Unauthorized | Missing or invalid credentials (Bearer token, Basic auth, or x_login/x_tran_key). |
404 | Not Found | The merchant does not exist. |
405 | Method Not Allowed | Any method other than the documented GET route. |
422 | Unprocessable Entity | The merchant account does not have a MID configured. |
502 | Bad Gateway | The payout backend could not be reached or returned an error. |
503 | Service Unavailable | The payout service is not configured for this account. |
filters are applied to the returned page only; use sort and pagination to narrow large result sets.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-11-19 00:00:00",
"run_last": "2026-10-19 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-20260919210912",
"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-20260919210912",
"run_next": "2026-10-19",
"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-20260919210912",
"interval": 2,
"interval_number": 1,
"run_until": 1,
"end_date": "2027-03-19"
}
}
{
"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-10-26"
}
}
When a scheduled recurring payment run fails (card declined or gateway transport error), the schedule's billing date is not changed:
run_next, run_count, and run_total are not advanced. The schedule only advances after a successful payment.status back to 1 (Active) via the update endpoint (after fixing the underlying issue, for example updating payment_id to a valid stored payment method). Reactivating resets the failure count, so the schedule gets a fresh 3 attempts.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. When the retry limit is reached, a recurring.paused event is emitted in addition to that attempt's recurring.payment_failed event. 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.
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Line item name. |
description | string | No | Line item description. |
quantity | number | No | Defaults to 1. |
unit_price | number | No | Price per unit. |
booking_id | string | No | ID of an appointment booking (from the Booking Module) to link this line item to. See the note below. |
{
"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
},
{
"title": "Haircut Appointment",
"quantity": 1,
"unit_price": 30.00,
"booking_id": "uuid"
}
]
}
}
booking_id on an item links that invoice to a booking from the Booking Module and automatically keeps the two in sync — no separate call is needed. Creating an invoice (or updating one to add the item) marks the booking as invoiced; cancelling the invoice before it's paid reverts the booking to its previous payment method so it can be invoiced again; and paying the invoice confirms payment on the booking itself. Only one outstanding invoice can be attached to a given booking at a time, and a booking already paid or already attached to a different invoice cannot be attached to a new one. This sync only affects the booking's status — it never blocks or fails the invoice action itself if the booking backend is unreachable.
{
"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.
All booking calls go through: /query/booking/{path}
Only the exact path-and-verb combinations documented below are routes. Anything else — an unrecognised path segment, an extra trailing segment, or an unsupported verb on a documented path — returns 404 with "Route not found". The one exception is the manage token flows and the two payment endpoints, which answer 405 for a wrong verb on an otherwise valid 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:
Only merchant accounts can currently log in and call this API — the payment gateway does not yet have a staff login flow.
Basic 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.GET /query/booking/services/{serviceId}/options and POST /query/booking/services/{serviceId}/price-preview with ?is_public=true — the two option endpoints an anonymous checkout needs in order to render the option form and show a running total. These are the only service sub-resources a public caller can reach; PUT .../options and every other service sub-resource stay merchant-scoped.POST /query/booking/holds and DELETE /query/booking/holds/{holdId} with is_public_booking: true — the customer-facing checkout hold flow. Omitting the flag keeps the caller's own identity, which is what a merchant-initiated hold wants.The /query/booking/manage/{token}/... flows remain fully unauthenticated — they are gated solely by the signed manage token in the URL.
Creating, updating, or cancelling a booking through any endpoint below — merchant/staff calls, the public booking-create flow, or the manage-token flows — automatically queues transactional emails through the gateway's own email system. No separate call is needed to trigger them. Up to three recipients are considered per event:
customerEmail snapshot (bookings taken without one, e.g. phone-only when emailRequired is off, are never emailed). Always sent regardless of merchant/staff preferences.notifyOnBookingCreated / notifyOnBookingUpdated / notifyOnBookingCancelled setting (see Upsert Merchant Settings below). Defaults to true when unset.notifyOnBooking* flags (see Staff below). Only a merchant can set these for a staff member — staff cannot set their own.These preference flags are stored on the booking backend but enforced entirely by the gateway — the booking backend itself does not send or suppress any notification. Customer emails that include a manage link point to the gateway's own hosted self-service page at /managebooking?token={manageToken}, which lets a customer view, reschedule, or cancel their booking with no gateway login — it consumes the same manage endpoints documented below.
A merchant can invoice a booking instead of charging a card, using the gateway's own Invoice API — set booking_id on an invoice line item to link it to a booking here. This keeps the booking's payment state in sync automatically; no separate call against this API is needed or possible for this sync (it isn't exposed as its own endpoint — the gateway calls the booking backend directly, server-side, whenever a linked invoice is created, cancelled, or paid):
paymentMethod to invoice. A booking that is already paid, or already linked to a different outstanding invoice, cannot be linked to another one.paymentMethod to what it was before, freeing it to be invoiced again.PATCH /{bookingId}/confirm-payment does (see below), reporting paymentMethod: "invoice".Use hasInvoice=false on List Bookings below to find bookings that are eligible to be invoiced (see that endpoint's query parameters).
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. |
optionValueIds | string | No | Comma-separated option value ids (a JSON array on POST /query/booking/holds). Duration effects only. Pass whatever the customer has already selected: without it, someone who picks a duration-lengthening option is shown slots their booking will not fit and gets a 409 at creation. |
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 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. 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. |
couponCode | string | No | A coupon code belonging to this merchant. There is no separate discountAmount input — if present, this endpoint validates the coupon itself (must exist, be enabled, under its redemption limit, and percent-off) and derives the discount from the coupon's percentage applied to the real subtotal; the caller cannot supply or influence the dollar amount directly. Omit entirely for no discount. |
quantity | integer | No | Seats/units. Defaults to 1. For a service with a definesUnits option group this is derived from the selected classes — sending a value that contradicts them is a 400. |
optionSelections | array | No | Up to 50 of { valueId, quantity? }. Ids and counts only — the server derives every label and amount, so a caller can never assert what an option costs. See Service Options. When present, a couponCode discount is computed against the option-adjusted subtotal rather than the bare service price. |
holdId | UUID | Conditional | Converts a checkout hold into the booking. Required for a public caller when the service has holdsRequired. Must match the hold's serviceId, scheduledAt, staffId and quantity exactly — any mismatch is a 409. See Checkout Holds. |
notes | string | No | Internal notes. |
New response fields when options are in play: snapshotCurrency, snapshotUnitsFromOptions, and an optionSelections[] array carrying an immutable snapshot of each selection (group and value key, label, effect definition, resolved amount) that survives the merchant later editing or deleting the option configuration.
Errors added: 400 for any invalid option selection, and 400 "holdId is required for this service" when holdsRequired is true and a public caller omits it. A 409 means the slot is gone — the hold expired, was already consumed, or another caller won the race — and should be presented to the customer as "this slot is no longer available", not as a generic failure.
{
"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,
"couponCode": "SUMMER10",
"quantity": 1,
"holdId": "uuid",
"optionSelections": [
{ "valueId": "uuid" }
]
}
}
{
"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,
"snapshotDiscountAmount": "5.00",
"snapshotCouponCode": "SUMMER10",
"totalPrice": "27.00",
"manageToken": "jwt"
}
}
taxRate/surchargeType/surchargeAmount (which the gateway trusts as-is, sourced from the merchant's own account settings), couponCode is independently validated by this endpoint against that merchant's coupons, and the resulting discount is always derived server-side from the coupon's percentage and the service's real price — never taken from the caller. This is what makes a coupon code safe to accept from a public, unauthenticated booking-create request in a way a raw dollar discount wouldn't be. The discount is applied to the subtotal before surcharge and tax, so surcharge/tax are computed off the already-discounted amount.
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.
{
"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",
"invoiceId": null,
"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 assigned staff member. |
serviceId | UUID | No | Filter by service. |
customerExternalId | integer | No | Filter by customer. |
paymentMethod | enum | No | inPerson, online, or invoice |
paymentStatus | enum | No | pending, paid, failed, refunded |
hasInvoice | "true" | "false" | No | Filter by whether a booking currently has an invoice attached (see Invoice Integration above). Combine with paymentStatus=pending&hasInvoice=false to find bookings eligible to be invoiced. |
Returns a single booking.
{
"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",
"invoiceId": null,
"snapshotName": "Haircut",
"snapshotPrice": "30.00",
"snapshotDurationMinutes": 60,
"snapshotDiscountAmount": "0.00",
"snapshotCouponCode": null,
"totalPrice": "32.40"
}
}
invoiceId is set (and paymentMethod becomes invoice) only via the Invoice Integration sync above — there's no way to set it directly through this endpoint.
Updates a booking. Merchant only.
| 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 | 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 | The caller's permission isn't merchant, or the authenticated user doesn't belong to this merchant account. |
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.
priceOverride) or changing quantity recomputes totalPrice from the current snapshotPrice, quantity, tax rate, and surcharge — and, if the booking has a stored snapshotDiscountAmount from a coupon applied at creation, that discount is reapplied against the new subtotal too (before surcharge/tax, same as at creation). This recalculation only happens while the booking is still pending. Once paymentStatus is paid, totalPrice — discount included — is frozen permanently; no later edit through this endpoint changes it again.
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 (or invoice, when paymentMethod is invoice) identifier. |
paymentMethod | "invoice" | No | Set when the payment being confirmed came from a paid invoice rather than a card charge (see Invoice Integration above). Sent automatically by the gateway when a linked invoice is paid — omit for the normal card-checkout flow. |
invoiceId | string | Conditional | Required together with paymentMethod: "invoice". Sent on every call, not only the first, so the invoice/booking link is re-established even if an earlier attach failed. |
{
"booking": {
"transactionExternalId": "txn_abc123"
}
}
{
"result": "success",
"booking": {
"id": "uuid",
"status": "confirmed",
"paymentMethod": "online",
"paymentStatus": "paid",
"transactionExternalId": "txn_abc123"
}
}
manageToken — a token is only ever returned once, at creation (or at reschedule). The gateway retains that original token internally so the deferred "booking confirmed" customer email (see Automated Email Notifications above) still carries a working manage link.
Marks an online payment as failed and cancels the booking. Merchant only. Idempotent when the booking is already in a failed state.
{
"result": "success",
"booking": {
"id": "uuid",
"status": "cancelled",
"paymentStatus": "failed"
}
}
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, together with the complete nested merchant and service records — everything a manage/cancel/reschedule UI needs in one call, with no separate merchant or service lookup required. Also flags whether self-service cancel and reschedule are still permitted.
{
"result": "success",
"booking": {
"id": "uuid",
"serviceId": "uuid",
"staffId": "uuid",
"customerFname": "Jane",
"customerLname": "Doe",
"customerEmail": "jane@example.com",
"customerPhone": "+15551234567",
"scheduledAt": "2027-06-01T09:00:00.000Z",
"endsAt": "2027-06-01T10:00:00.000Z",
"timeZone": "America/Los_Angeles",
"quantity": 1,
"status": "pending",
"paymentMethod": "online",
"paymentStatus": "pending",
"snapshotName": "Haircut",
"snapshotPrice": "30.00",
"snapshotDurationMinutes": 60,
"snapshotDiscountAmount": "0.00",
"snapshotCouponCode": null,
"totalPrice": "30.00",
"notes": "",
"merchant": {
"id": "uuid",
"externalId": 1042,
"...": "the full merchant settings record — see Get Merchant Settings below"
},
"service": {
"id": "uuid",
"name": "Haircut",
"price": "30.00",
"durationMinutes": 60,
"...": "the full service record — see Services below"
},
"canCancel": true,
"canReschedule": true
}
}
booking.merchant.externalId is this gateway's own numeric merchant id — this response alone is enough to resolve which merchant the booking belongs to, with no merchant id ever needed in the URL. canCancel and canReschedule are merged directly onto the booking object, not returned as a separate top-level key.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.
{
"result": "success",
"booking": {
"id": "uuid",
"status": "cancelled"
}
}
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 immediately. This response is the plain, flat booking row (the same shape as PATCH /query/booking/{bookingId}) — it does not include the nested merchant/service records that GET /query/booking/manage/{token} returns.
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 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 |
notifyOnBookingCreated | boolean | true | Whether the merchant is emailed when a new booking is created. See Automated Email Notifications above — enforced by the gateway, not the booking backend. |
notifyOnBookingUpdated | boolean | true | Whether the merchant is emailed when a booking is updated (including reschedules). |
notifyOnBookingCancelled | boolean | true | Whether the merchant is emailed when a booking is cancelled. |
{
"merchant": {
"acceptsInPersonPayment": true,
"acceptsOnlinePayment": true,
"bookingService": true,
"serviceLabel": "Appointment",
"timeStepMinutes": 30,
"leadTimeMinutes": 120,
"cancelWindowHours": 24,
"allowStaffPricingOverrides": false,
"notifyOnBookingCreated": true,
"notifyOnBookingUpdated": true,
"notifyOnBookingCancelled": true
}
}
Returns the merchant's weekly opening hours as an array of schedule entries. Returns an empty array if no schedule has been set yet. Merchant only.
{
"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. Merchant only.
{
"result": "success",
"exceptions": [
{ "id": "uuid", "date": "2026-12-25", "isOpen": false, "openTime": null, "closeTime": null }
]
}
Returns a single date exception by ID. Merchant only.
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. Merchant only.
Creates a service record. Merchant only.
| 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. Also accessible with public permission via ?is_public=true — public callers receive all merchant services, 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. Merchant only.
Updates a service. Merchant only.
| 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. Merchant only.
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. Merchant only. 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. Merchant only. Requires merchant allowStaffPricingOverrides = true. 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
}
}
Options let a service carry customer-selectable add-ons that change its price, its duration, or both — Long hair +$20, Add gift wrap +$3, or adult/child fare classes that also determine how many units the booking covers. Options are grouped; a group controls how many of its values a customer may pick.
Only ids and counts are ever sent to these endpoints. Every label and amount is derived on the server from the merchant's own configuration, so a caller can never assert what an option costs.
Returns the service's option groups, each with its values nested inside, ordered by sortOrder. Also accessible with public permission via ?is_public=true — public callers receive active groups and active values only, because a customer must not be offered something they cannot select. Merchant and staff callers see inactive entries too.
{
"result": "success",
"options": [
{
"id": "uuid",
"serviceId": "uuid",
"key": "hair_length",
"label": "Hair length",
"description": null,
"selectionMode": "single",
"minSelections": 0,
"maxSelections": 1,
"definesUnits": false,
"sortOrder": 0,
"isActive": true,
"values": [
{
"id": "uuid",
"groupId": "uuid",
"key": "long",
"label": "Long",
"description": null,
"isDefault": false,
"isActive": true,
"sortOrder": 0,
"priceEffectType": "fixed",
"priceEffectValue": "20",
"priceEffectScope": "perUnit",
"durationEffectType": "delta",
"durationMinutes": 15
}
]
}
]
}
| Field | Type | Description |
|---|---|---|
selectionMode | single | multiple | How many values a customer may pick. A single group must have maxSelections: 1. |
minSelections | integer | Minimum a customer must pick. This is the only option rule that fires on absence, so it is the only one that can reject a caller that has not changed — see the deployment note below. |
maxSelections | integer | Maximum a customer may pick. |
definesUnits | boolean | When true, this group supplies the booking's quantity — adult/child fare classes, for example. At most one active definesUnits group per service, and it requires selectionMode: multiple, minSelections >= 1, all-perUnit price scopes, no duration effects and no defaults. |
description | string | Optional, and optional means absent, not null. The schema is z.string().optional(), so an explicit null is rejected with 422 "Invalid input: expected string, received null" — omit the key instead. Note that GET returns "description": null when unset, so a GET response cannot be fed straight back into the PUT. Omitting the key still clears a previously-set description, because the PUT recreates every group. Same rule for a value's description. |
| Field | Type | Description |
|---|---|---|
priceEffectType | fixed | percentage | null | How priceEffectValue is applied. A percentage effect must use perUnit scope, and is measured against the service price — never a running subtotal. See How a percentage is measured below. |
priceEffectValue | number in, string out | The amount or percentage. Send it as a JSON number (20) — a quoted string is rejected with "Invalid input: expected number, received string". Responses serialise it back as a string ("20"). |
priceEffectScope | perUnit | perLeg | null | perUnit scales with quantity; perLeg is charged once per booking. |
durationEffectType | delta | override | null | delta adds durationMinutes to the service duration; override replaces it. |
durationMinutes | integer | null | The duration effect's value. |
isDefault | boolean | Pre-selected in the widget. At most one active default per single-select group. |
A price effect and a duration effect must each be wholly present or wholly absent: sending priceEffectType without priceEffectValue and priceEffectScope is rejected. For a value with no effects, omit the effect keys rather than sending them as null (as the short value above does).
Validation failures come back as 422 with a field path per problem, indexing into the arrays — e.g. groups[0].values[1].priceEffectValue.
A percentage effect is applied to the service price × quantity, fixed before any option is applied. It is not applied to a running subtotal, so percentages never compound with each other and are never affected by the fixed effects selected alongside them. Selection order is therefore irrelevant — that is the point of the design.
Worked example, on a $50 service at quantity 1 with three options selected:
| Selected | Effect | Contribution | Running total |
|---|---|---|---|
| — | service price × quantity | 50 × 1 | 50.00 |
| Long hair | fixed 20, perUnit | 20 × 1 | 70.00 |
| Premium finish | percentage 10, perUnit | 50 × 10 / 100 = 5.00 | 75.00 |
| Gift wrap | fixed 3, perLeg | 3, once | 78.00 |
The 10% is $5.00, not $7.00 — the +$20 is not part of the basis. Two 10% options on a $100 service add $20, not $21.
Because the basis includes quantity, a perUnit percentage scales with it: the same 10% on the $50 service at quantity 2 contributes 100 × 10 / 100 = $10, while a perLeg effect stays flat.
One exception. For a service whose quantity comes from a definesUnits group, the basis is the sum of the selected unit-class totals rather than price × quantity. Each class total is (service price + that class's own effect) × its count, so the class-level effects are inside the basis and an ordinary percentage option does include them.
On a $30 service whose fare group offers Adult (no effect) and Child (fixed −10, perUnit), picking 2 adults and 1 child gives 30 × 2 + 20 × 1 = a basis of $80, so a separate 10% option contributes $8 and the subtotal is $88.
Atomic full replace, not a merge — every call deletes the service's existing option groups and recreates the submitted list, the same way PUT /query/booking/services/{serviceId}/staff behaves. { "groups": [] } clears everything. Merchant only; staff callers additionally need canEditService.
To clear a service's options, send this PUT with { "groups": [] }. There is deliberately no DELETE on this path — DELETE /query/booking/services/{serviceId}/options returns 404, as does any other verb.
Structural rules are validated as a whole, so violations come back as 422 with the field path that failed.
{
"options": {
"groups": [
{
"key": "hair_length",
"label": "Hair length",
"selectionMode": "single",
"minSelections": 0,
"maxSelections": 1,
"definesUnits": false,
"sortOrder": 0,
"isActive": true,
"values": [
{
"key": "short",
"label": "Short",
"isDefault": false,
"isActive": true,
"sortOrder": 0
},
{
"key": "long",
"label": "Long",
"isDefault": false,
"isActive": true,
"sortOrder": 1,
"priceEffectType": "fixed",
"priceEffectValue": 20,
"priceEffectScope": "perUnit",
"durationEffectType": "delta",
"durationMinutes": 15
}
]
}
]
}
}
Prices a set of option selections without creating anything — stateless and side-effect free. Drives the running total in the widget, and is the same source the gateway uses to resolve the trusted subtotal a percentage coupon is applied to. Also accessible with public permission via ?is_public=true.
This endpoint accepts exactly three fields and rejects anything else with 422. It deliberately returns no tax, surcharge, coupon or grand total: the gateway owns everything layered above the base price, and that split is the reason the endpoint is this narrow. Do not send taxRate, surchargeAmount, discountAmount or couponCode.
This is the only correct source for a displayed total. Reimplementing the arithmetic client-side drifts from what is actually charged — percentage effects in particular are measured against a fixed basis rather than a running subtotal, so they add instead of compounding. See How a percentage is measured under GET /options above.
| Field | Type | Required | Description |
|---|---|---|---|
staffId | UUID | No | Prices against that staff member's priceOverride / durationMinutesOverride when the merchant has allowStaffPricingOverrides. Rejected with 400 for a later-mode service; 404 when the staff member is not assigned to the service. |
quantity | integer | No | Positive integer, defaults to 1. Omit for a definesUnits service — quantity is derived from the selected classes there, and a value that disagrees is a 400. |
optionSelections | array | No | Up to 50 of { valueId, quantity? }. The inner quantity is only meaningful for a definesUnits class. |
{
"pricePreview": {
"staffId": "uuid",
"quantity": 2,
"optionSelections": [
{ "valueId": "uuid" },
{ "valueId": "uuid", "quantity": 1 }
]
}
}
{
"result": "success",
"pricePreview": {
"currency": "USD",
"basePrice": "45",
"unitPrice": "55",
"perBookingTotal": "3",
"subtotal": "113",
"quantity": 2,
"durationMinutes": 75,
"selections": [
{
"optionGroupId": "uuid",
"optionValueId": "uuid",
"groupKey": "hair_length",
"groupLabel": "Hair length",
"valueKey": "long",
"valueLabel": "Long",
"quantity": 1,
"amount": "20"
}
]
}
}
| Field | Description |
|---|---|
basePrice | The service price before any option effect. |
unitPrice | Per-unit price after perUnit effects. An average when a definesUnits group mixes classes at different prices. |
perBookingTotal | The sum of perLeg effects, charged once per booking rather than per unit. |
subtotal | The amount to price against. Already equals unitPrice x quantity + perBookingTotal — do not multiply it by quantity again. |
durationMinutes | The option-adjusted duration. Pass the same selections to /slots so the customer is only offered slots the booking will actually fit. |
Errors: 400 invalid selection (unknown, inactive or foreign valueId; group min/max violated; duplicate value; quantity on a non-unit option; quantity contradicting the price classes; competing duration overrides; a negative resulting price) · 404 service or staff not found · 422 malformed UUID or an unrecognised field.
A hold reserves capacity for one slot while a customer completes checkout, so a contended slot cannot be taken from under them between picking a time and paying. GET /query/booking/services exposes holdsRequired; when it is true, a public booking submitted without a holdId is rejected with 400. merchant and staff callers bypass that check — a trusted operator is not racing an anonymous customer.
sessionRef is not a caller-supplied field. The gateway generates it server-side with a CSPRNG, stores it in the checkout's session, and reuses the same value for every re-hold in that checkout. Anything a client sends under that name is discarded. It is never returned in any response, and the booking backend stores only a SHA-256 hash of it. This matters because hold supersession is keyed entirely on merchantId + sessionRef: whoever holds a sessionRef can release that customer's reservation and take the slot.
Reserves the slot and returns an expiresAt to drive a countdown in the widget. Creating a hold releases every other active hold for the same checkout, so a customer who changes their slot or their options just creates another hold rather than needing to release the old one first. Pass is_public_booking: true for the customer-facing widget flow; omit it for a merchant-initiated hold, which keeps the caller's own identity.
| Field | Type | Required | Description |
|---|---|---|---|
is_public_booking | boolean | No | Set true for customer-widget holds to scope to public permission. |
serviceId | UUID | Yes | Service being held. |
staffId | UUID | Conditional | Required for atCreation services; must be omitted for later — the same rule as booking creation. |
scheduledAt | ISO 8601 datetime | Yes | Slot start, minute precision (no seconds) — and it must carry an offset, e.g. 2027-06-01T09:00Z. The schema is z.iso.datetime({ precision: -1 }), which rejects a bare local datetime with a 422. Send the UTC instant here and the customer's zone in timeZone, exactly as POST /query/booking does — the hold must resolve to the same instant as the booking or redeeming it is a 409. |
quantity | integer | No | Positive integer, defaults to 1. |
timeZone | string | Yes | Timezone identifier. |
optionValueIds | UUID[] | No | Up to 50 option value ids, as a JSON array (unlike the comma-separated string /slots takes). Duration effects only, so the hold reserves the window the booking will actually occupy. Group min/max are not enforced here — a partial selection mid-checkout is expected. |
ttlSeconds | integer | No | 30–900. Omit this. The merchant's holdTtlSeconds is both the default and the ceiling, and a larger request is clamped down silently rather than rejected — so a hardcoded value produces confusing behaviour across merchants. |
sessionRef | — | — | Do not send. Generated by the gateway; any client value is discarded. See the note above. |
{
"is_public_booking": true,
"hold": {
"serviceId": "uuid",
"staffId": "uuid",
"scheduledAt": "2027-06-01T09:00Z",
"timeZone": "UTC",
"quantity": 1,
"optionValueIds": ["uuid"]
}
}
{
"result": "success",
"hold": {
"id": "uuid",
"merchantId": "uuid",
"serviceId": "uuid",
"staffId": "uuid",
"scheduledAt": "2027-06-01T09:00:00.000Z",
"endsAt": "2027-06-01T10:30:00.000Z",
"timeZone": "UTC",
"quantity": 1,
"expiresAt": "2027-05-20T12:05:00.000Z",
"consumedAt": null,
"releasedAt": null,
"bookingId": null,
"createdAt": "2027-05-20T12:00:00.000Z",
"updatedAt": "2027-05-20T12:00:00.000Z"
}
}
endsAt is option-adjusted, so a hold whose window was lengthened by a duration option reserves the longer window. Drive the customer-facing countdown from expiresAt.
Errors: 400 staffId rule violation or bad optionValueIds · 403 booking service disabled for the merchant · 404 merchant, service or staff not found · 409 slot unavailable · 422 validation.
Releases the reservation when the customer abandons checkout or navigates back. Idempotent by design — releasing an already-released or already-expired hold is a 200 no-op, because the caller's intent is already satisfied, so it is safe to call blind without tracking whether the hold is still live.
Errors: 404 unknown hold, or one belonging to another merchant · 409 the hold has already been consumed into a booking · 422 malformed UUID.
holdsRequired from GET /query/booking/services.GET /query/booking/slots, passing optionValueIds if duration-affecting options are already chosen.expiresAt.POST /query/booking with holdId plus the full optionSelections. The hold must describe the same serviceId, scheduledAt, staffId and quantity — any mismatch is a 409.DELETE /query/booking/holds/{holdId}.409 at booking creation, present it as "this slot is no longer available" and re-fetch slots. It means the hold expired, was already consumed, or another caller won the race — not a generic failure.Hold consumption is an atomic compare-and-swap inside the booking transaction, so two concurrent requests redeeming the same holdId produce exactly one booking and the loser gets a clean 409. A double-submitted checkout that carries a holdId therefore cannot create a duplicate booking.
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, for reference. Staff login is not yet supported by the payment gateway. |
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. |
notifyOnBookingCreated | boolean | No | Whether this staff member is emailed when a booking assigned to them is created. Defaults to true. See Automated Email Notifications above. |
notifyOnBookingUpdated | boolean | No | Whether this staff member is emailed when a booking assigned to them is updated (including reschedules). Defaults to true. |
notifyOnBookingCancelled | boolean | No | Whether this staff member is emailed when a booking assigned to them is cancelled. Defaults to true. |
{
"staff": {
"firstName": "Jane",
"lastName": "Smith",
"email": "jane@example.com",
"userExternalId": 101,
"color": "#FF5733",
"notifyOnBookingCreated": true,
"notifyOnBookingUpdated": true,
"notifyOnBookingCancelled": true
}
}
Returns all staff under the merchant. Merchant tokens receive full records. Also accessible with public permission via ?is_public=true — public callers receive each entry reduced to { id, firstName, lastName, color } (no email).
Returns a staff member. Merchant tokens get the full record. Also accessible with public permission via ?is_public=true — public callers receive { id, firstName, lastName, color } only (no email, no sub-resources).
Updates a staff record. Merchant only.
| 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. |
notifyOnBookingCreated | boolean | See Automated Email Notifications above. |
notifyOnBookingUpdated | boolean | |
notifyOnBookingCancelled | boolean |
{
"staff": {
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@example.com",
"color": "#FF5733",
"isActive": true,
"notifyOnBookingCreated": true,
"notifyOnBookingUpdated": true,
"notifyOnBookingCancelled": true
}
}
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}. Merchant only.
{
"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. Merchant only.
{
"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. Merchant only.
| 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. Merchant only.
{
"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. Merchant only.
Adds a recurring break window to a specific day of the week. Merchant only.
| 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. Merchant only. 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. Merchant only.
Returns all date exceptions for the staff member. Returns an empty array if none have been created. Merchant only.
{
"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. Merchant only.
Adds or updates a one-off availability override for a staff member (day off or special hours). Merchant only. Idempotent on the same date — calling again with the same date updates the existing record, including replacing its breaks (see below).
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. Merchant only.
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 callers.manage flows use the signed manage token returned at booking creation as their sole credential. Do not expose manage tokens unnecessarily.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.Transaction Supporting Documents & Dispute Evidence
The Evidence API stores photo/file evidence tied to a transaction, and exposes chargeback/retrieval (dispute) data alongside the ability to accept or challenge an open dispute with that evidence. Both areas are served by the gateway's /query/evidence endpoint, which resolves the caller's identity, mints a module JWT, and forwards the request to the evidence locker backend.
All evidence calls go through: /query/evidence/{path}
All responses follow the ExpiEndpoint envelope — the payload is returned under a named resource key alongside "result". The resource key varies by endpoint (media, documents, dispute, disputes):
{ "result": "success", "media": { ... } }
{ "result": "success", "dispute": { ... } }
{ "result": "success", "disputes": [ ... ], "current_page": 1, "per_page": 10, "last_page": 1, "total": 1 }
Upload/response endpoints (document upload, dispute response) are multipart/form-data — every other request/response body is JSON.
Every endpoint 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 permission forwarded to the backend:
sysad → sysAdminmerchant → merchantenhanced → enhancedteammember (staff) → staffmerchant and enhanced callers are additionally checked against the merchant in the URL — the request is rejected with 403 if that user doesn't have access to that specific merchant account. sysad and staff callers aren't scoped to a single merchant.
Basic Auth alone (no session/Bearer user resolved) also succeeds and runs with merchant permission by default — the same fallback the Booking Module uses. Only a request with neither a resolvable user nor valid x_login/x_tran_key credentials is rejected with 401.
Every transaction can have supporting photo evidence (receipts, signed slips, ID, etc.) attached under a "group" identified by the transaction's own ID. A group has a storage cap — maxPhotos images and maxBytes total — enforced by the backend and reported back in groupSummary.
groupId is the transaction's ID. Returns every document uploaded to that transaction's group, plus a groupSummary describing how much of the group's storage is used.
{
"result": "success",
"media": [
{
"id": "uuid",
"fileName": "receipt.jpg",
"signedUrl": "https://.../receipt.jpg?signature=..."
}
],
"groupSummary": {
"photoCount": 1,
"maxPhotos": 20,
"totalBytes": 482113,
"maxBytes": 9437184,
"isSubmitted": false
}
}
signedUrl is a temporary, pre-signed link directly to the file — it can be used as-is (e.g. as an <img src>) without a separate download call.
Uploads one or more files to a transaction's group. Request body is multipart/form-data.
| Field | Type | Required | Description |
|---|---|---|---|
groupId | string | Yes | The transaction ID to attach documents to. |
photos[] | file(s) | Yes | One or more files. See limits below. |
Accepted types: JPEG, PNG, WebP, HEIC, HEIF, PDF. Per the dashboard uploader's own limits: up to 5 files per request, 3 MB per file, 9 MB total per group (subject to the group's remaining maxPhotos/maxBytes as reported by groupSummary).
{
"result": "success",
"media": {
"id": "uuid",
"fileName": "receipt.jpg"
}
}
Returns a single document's metadata.
Returns a temporary download link for a single document — used for viewing/downloading rather than embedding a persistent URL.
{
"result": "success",
"media": {
"url": "https://.../receipt.jpg?signature=...",
"fileName": "receipt.jpg",
"fileType": "image/jpeg"
}
}
Permanently removes a document from its transaction's group.
Chargebacks and retrievals (disputes) filed against a merchant's account. List disputes, retrieve a single dispute, view/download its attached documents, and respond to an open dispute by accepting it or challenging it with evidence. Only merchants with a MID configured on their account can use these endpoints.
Returns a paginated list of disputes for the merchant identified by {mid}.
| Field | Type | Required | Description |
|---|---|---|---|
page | integer | No | Defaults to 1. |
per_page | integer | No | 10, 25, or 50. Defaults to 10. |
{
"result": "success",
"disputes": [ { "...": "see Get Dispute below for the full shape of each entry" } ],
"current_page": 1,
"per_page": 10,
"last_page": 1,
"total": 1
}
current_page/per_page/last_page/total are returned as top-level siblings of disputes, not nested inside it.
Returns a single dispute.
| Field | Description |
|---|---|
id | Dispute ID. |
case_number | Case number. |
case_status / status | Current status (e.g. "Needs Response", "Open", "Won", "Lost", "Pending", "Closed"). |
item_type / case_type | Chargeback or retrieval. |
reason_code / reason_description | Network reason code and its description. |
case_amount / currency | Disputed amount. |
created_date / due_date | When the case was opened, and the merchant's response deadline. |
transaction_id | Associated gateway transaction ID, if matched. |
cardholder_account_number / card_name | Masked card number and card brand. |
transaction_date / posted_date | Original transaction and posting dates. |
auth_code / order_id / arn | Authorization code, order ID, and acquirer reference number, when available. |
mid / dba_name / legal_name / mcc / bank / association | Merchant/processing details as recorded on the case. |
merchant_comments | Action requested of the merchant, when present. |
notes | Array of { type, note, created_at } case notes. |
Downloads a single dispute document's raw content.
{
"result": "success",
"dispute": {
"content": "<base64-encoded file bytes>",
"content_type": "image/jpeg"
}
}
content so they survive the JSON response — decode it client-side before writing/serving the file.
Accepts or challenges an open dispute. Request body is multipart/form-data.
| Field | Type | Required | Description |
|---|---|---|---|
disputeId | string | Yes | The dispute to respond to. |
disputeAction | accept | challenge | Yes | Whether to accept the chargeback or challenge it with evidence. |
comment | string | No | Additional context for the response. |
photos[] | file(s) | Conditional | Evidence files. Required when disputeAction is challenge and the dispute has no documents already on file. Ignored for accept. |
disputeAction, not action — action is a routing-reserved key that the gateway strips from every /query/evidence/... request before it reaches this endpoint, regardless of value. Sending action instead of disputeAction silently drops the field and fails as if it were never provided at all.
Accepted file types, count, and size limits match the Transaction Supporting Documents group above: JPEG, PNG, WebP, HEIC, HEIF, PDF — up to 5 files, 3 MB per file, 9 MB total.
{
"result": "success",
"dispute": { "...": "the updated dispute — see Get Dispute above" }
}
| Status | Cause |
|---|---|
200 | Response recorded. |
401 | Missing or invalid token. |
403 | Caller doesn't belong to the merchant that owns this dispute. |
404 | Merchant or dispute not found. |
422 | disputeAction missing or not one of accept/challenge ("action must be 'challenge' or 'accept'." — the error message itself still refers to it as "action" since that's the field name the evidence locker backend sees after translation), or the merchant account does not have a MID configured. |
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.