Expi API Documentation

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.

Base URL: https://your-domain.com/query

Authentication

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.

Quick Checklist
  • Credentials are present on every request.
  • Only one auth method is used per request.
  • Secrets never appear in client-side code.
Auth Endpoints
  • POST /query/auth/mfatoken — Validates username/password and returns an mfa_token.
  • POST /query/auth/token — If payload contains mfa_token and code, verifies MFA and issues an access token.
  • POST /query/auth/token — If payload contains username (or email) and password, it also requires trusted-device credentials to issue an access token.
  • GET /query/me — Returns the current authenticated user, merchant, and organization context (requires authentication).
Example: Login Step 1 (POST /query/auth/mfatoken)
{
  "username": "user@example.com",
  "password": "your_password"
}
MFA Challenge Response (HTTP 200)
{
  "result": "success",
  "mfa_token": "dummy_mfa_token",
  "expires_in": 300
}
Example: Login Step 2 (POST /query/auth/token)
{
  "mfa_token": "dummy_mfa_token",
  "code": "123456",
  "remember_device": 1
}
Example: Trusted Login (POST /query/auth/token)
{
  "username": "user@example.com",
  "password": "your_password",
  "trusted_device_id": "32_hex_device_id",
  "trusted_device_token": "device_token"
}
MFA Success (remembered device)
{
  "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
}
Example: Me (GET /query/me)
Authorization: Bearer <access_token>
Option 1: Basic Auth (Header)
Authorization: Basic <base64(username:secret_key)>

Base64 must be generated from the exact string username:secret_key (one colon, no extra spaces).

Option 2: Body Parameters
{
  "x_login": "your_username",
  "x_tran_key": "your_secret_key"
}
Common Pitfalls
  • Wrong key: Make sure you use the secret key, not a public identifier.
  • Mixed methods: Do not send both Basic Auth and body credentials in the same request.
  • Invalid JSON: Trailing commas or comments will cause auth to fail.
  • MFA challenge shape: An MFA-required login returns result: "success" with mfa_token and expires_in.
  • Remember device default: If remember_device is omitted during MFA verification, the device is remembered by default.
  • Trusted login requirements: The username/password token flow requires both trusted_device_id and trusted_device_token; otherwise it fails with Unknown Device.
Security Tips
  • Store secrets in server-side environment variables.
  • Rotate keys regularly and after staff changes.
  • Never log raw credentials in application logs.

REST API

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.

How It Works

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:

  • Charges and Voids - Create charges, capture auths, and void eligible transactions.
  • Refunds - Issue credits against existing charges.
  • Customers – Create profiles, store billing details, and manage customer data.
  • Invoices – Generate invoices, track status, and collect payments.
  • Products – Define items and pricing used across invoices and recurring.
  • Card Issuing – Issue cards, fund balances, and fetch card activity.
  • Recurring – Automate scheduled billing with flexible intervals.
Optional Pagination Query Parameters

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.

Tokens

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

POST /tokens Create Card Token
Body Parameters
Field Type Required Description
numberstringYes16-digit card number.
monthstringYes2-digit month 0112.
yearstringYes2-digit year YY.
x_loginstringNoMerchant login (only if not using Authorization header).
x_tran_keystringNoMerchant tran key/secret (only if not using Authorization header).
Create Token Body
{
  "number": "4242424242424242",
  "month": "12",
    "year": "30"
}
Sample Response
{
  "result": "success",
  "token": "tok_..."
}

Charges

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.

GET /charges List Charges
POST /charges Create a Charge
Create Charge Body Parameters
Field Type Required Description
tokenstringConditionalToken from /tokens. Required unless charging with payment_method_id.
payment_method_idnumberConditionalExisting saved payment method id (CustomerDetails). Required unless charging with token. Aliases: x_payment_id, paymentmethodid.
cvcstringConditionalRequired for token-based charges. Provide at charge time (do not store). You can also pass billing.cvc.
amountnumberYesCharge amount.
transtypestringNoDefaults to AUTH_CAPTURE. Use AUTH_ONLY for auth-then-capture flows.
billingobjectNoBilling/contact fields (first_name, last_name, address, city, state, zip, country, phone, email, description).
customer_idnumberNoOptional customer id. When charging with payment_method_id, if provided it must match the payment method’s customer.
save_payment_methodbooleanNoIf true, creates a customer/payment method (if needed) and charges it. Requires token-based charge (raw card data is needed).
use_customer_profilebooleanNoIf 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_loginstringNoMerchant login (only if not using Authorization header).
x_tran_keystringNoMerchant tran key/secret (only if not using Authorization header).
GET /charges/{id} Retrieve a Charge
PUT /charges/{id} Update a Charge
POST /charges/{id}/capture Capture a Charge
GET /charges/search?q={query} Search Charges
Create Charge (Token) Body
{
    "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"
    }
}
Create Charge (Payment Method) Body
{
  "payment_method_id": 12345,
  "customer_id": 67890,
  "amount": 12.34,
  "transtype": "AUTH_CAPTURE"
}
Create Charge + Save Payment Method
{
  "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"
  }
}
Create Charge (Sample Response)
{
  "result": "success",
  "charge": {
    "id": 123456,
    "transaction_id": 98765,
    "amount": 12.34,
    "currency": "USD",
    "status": 1,
    "status_text": "succeeded",
    "type": 1,
    "created": "...",
    "description": "Online Purchase",
    "notes": null,
    "reference_id": 0,
    "last4": "4242"
  },
    "customer_id": 67890,
    "payment_method_id": 12345,
  "transaction": {
    "status": 1,
    "status_text": "Success",
    "reason_text": "...",
    "transaction_id": "123456"
    }
}
Capture Charge Body Parameters
Field Type Required Description
amountnumberNoOptional capture amount (omit for full capture where supported).
x_loginstringNoMerchant login (only if not using Authorization header).
x_tran_keystringNoMerchant tran key/secret (only if not using Authorization header).
Capture Charge Body
{
  "amount": 12.34
}
HTTP Status Codes
Code Meaning When it is returned
200OKSuccessful list, retrieve, search, update, and approved create/capture requests. Idempotent replays return the original response with this or the originally stored code.
400Bad RequestInvalid JSON body; missing amount or token/payment_method_id; invalid or expired token; invalid card number, expiration, or CVC; missing cvc on a token-based charge; save_payment_method without a token; update with no supported fields; invalid Idempotency-Key.
401UnauthorizedMissing or invalid credentials (Bearer token, Basic auth, or x_login/x_tran_key).
403ForbiddenThe authenticated merchant account is disabled.
404Not FoundCharge, merchant, customer, or payment method not found (or not owned by the authenticated merchant); unrecognized route.
405Method Not AllowedHTTP method not supported on the route (e.g. DELETE /charges, GET /charges/{id}/capture).
409ConflictA request with the same Idempotency-Key is still processing. Includes a Retry-After header.
422Unprocessable EntityThe gateway declined the charge or capture (see transaction.reason_text), or an Idempotency-Key was reused with a different payload.
429Too Many RequestsRate limit exceeded. Includes a Retry-After header and rate limit headers.
500Internal Server ErrorServer-side configuration or processing error (e.g. token key not configured).
502Bad GatewayThe upstream payment gateway could not be reached.

Transactions

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.

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

GET /query/transaction List Transactions

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.
List Query Parameters
Field Type Required Description
pageintegerNoOptional page number for list results.
pageSizeintegerNoOptional page size for list results.
filtersstringNoOptional filter expression (same behavior as other Expi list endpoints).
sortstringNoOptional sort key (for example -uniqueID).
modifiersstringNoOptional comma-separated response field projection.
List Transactions Example
GET /query/transaction?page=1&pageSize=25&sort=-uniqueID
GET /query/transaction/{id} Retrieve a Transaction
POST /query/transaction Create a Transaction Record
Create Transaction Body Parameters
Field Type Required Description
amount_total / amountnumberYesTransaction amount. amount_total matches the response structure.
amount_taxnumberNoOptional tax amount.
amount_discountnumberNoOptional discount amount.
amount_shippingnumberNoOptional shipping amount.
amount_surchargenumberNoOptional surcharge amount.
amount_tipnumberNoOptional tip amount.
currencystringNoOptional currency code (for example USD).
descriptionstringNoOptional description.
notesstringNoOptional notes.
coupon_idnumberNoOptional coupon id association.
recurring_idnumberNoOptional recurring/subscription id association.
transtypestring|numberNoDefaults to AuthCapture. Accepts string or numeric code.
statusstring|numberNoNot accepted on create. Always stored as Unprocessed.
statestring|numberNoDefaults to Unknown. Accepts string or numeric code.
sourcestring|numberNoDefaults to Endpoint. Accepts string or numeric code; response returns the human-readable label (for example, Endpoint).
created / transdatestringNoOptional transaction timestamp (if omitted, DB/defaults apply). created matches the response structure.
invoicing_id / invoice_idnumberNoOptional link to an invoice.
invoiceobjectNoIf provided and invoice id is omitted (invoicing_id / invoice_id), an invoice is created and linked.
items / line_itemsarrayNoOptional 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_idnumberNoCustomer association. If omitted, a customer record is created from customer or billing/shipping.
customerobjectNoOptional customer payload used to create a customer when customer_id is omitted.
payment_information.methodnumber|stringNoNon-sensitive payment method indicator (ex: CC, echeck, or numeric code).
payment_information.last4stringNoNon-sensitive last 4 digits (stored as provided digits only).
billingobjectNoOptional billing/contact fields (first_name, last_name, address, city, state, zip, country, phone1, phone2, email). phone is also accepted as an alias for phone1.
shippingobjectNoOptional shipping/contact fields (first_name, last_name, address, city, state, zip, country, phone1, phone2, email). phone is also accepted as an alias for phone1.
PUT /query/transaction/{id} Update a Transaction
DELETE /query/transaction/{id} Delete a Transaction
Create Transaction Body
{
    "amount_total": 12.34,
    "amount_tax": 0.50,
    "amount_discount": 1.00,
    "amount_shipping": 2.50,
    "amount_surcharge": 0.25,
    "amount_tip": 1.50,
    "currency": "USD",
    "description": "Order #1001",
    "notes": "Recorded from mobile tap-to-pay",
    "transtype": "AuthCapture",
    "state": "Settled",
    "created": "2026-01-29 12:34:56",
    "payment_information": {
        "method": "CC",
        "last4": "4242"
    },
    "billing": {
        "first_name": "Jane",
        "last_name": "Smith",
        "email": "jane@example.com",
        "phone1": "555-555-5555"
    },
    "items": [
        {"title": "T-Shirt", "quantity": 1, "unit_price": 12.34}
    ]
}
Transaction (Sample Response)
{
  "result": "success",
  "transaction": {
    "uniqueID": 123456,
    "customer_id": 555,
    "coupon_id": 0,
    "recurring_id": 0,
    "amount_total": 12.34,
    "amount_tax": 0,
    "amount_discount": 12.34,
    "amount_shipping": 0,
    "amount_surcharge": 0,
    "amount_tip": 0,
    "currency": "USD",
    "created": "2026-01-29 12:34:56",
    "description": "Order #1001",
    "notes": "Created by integration",
    "billing": {
      "first_name": "Jane",
      "last_name": "Smith",
      "email": "jane@example.com",
      "phone1": "555-555-5555",
      "phone2": "",
      "address": "",
      "address2": "",
      "city": "",
      "state": "",
      "zip": "",
      "country": ""
    },
    "shipping": {
      "first_name": "",
      "last_name": "",
      "email": "",
      "phone1": "",
      "phone2": "",
      "address": "",
      "address2": "",
      "city": "",
      "state": "",
      "zip": "",
      "country": ""
    },
    "payment_information": {
      "method": 16,
      "last4": "4242"
    },
    "invoicing_id": null,
    "reference_id": null,
    "status": "Unprocessed",
    "state": "Settled",
    "transtype": "AuthCapture",
    "source": "Endpoint"
  }
}

Refunds

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.

POST /refunds Create a Refund
Create Refund Body Parameters
Field Type Required Description
charge_idnumberYesOriginal charge Transaction presentation_id. Alias: transaction_id.
amountnumberYesRefund amount. Must be greater than 0 and not exceed the original charge amount.
notesstringNoOptional notes sent to the gateway.
x_loginstringNoMerchant login (only if not using Authorization header).
x_tran_keystringNoMerchant tran key/secret (only if not using Authorization header).
Create Refund (Body)
{
  "charge_id": 123456,
  "amount": 12.34,
  "notes": "Customer requested refund"
}
Create Refund (Sample Response)
{
  "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"
  }
}
HTTP Status Codes
Code Meaning When it is returned
200OKThe refund (gateway CREDIT) was approved. Idempotent replays return the original response with the originally stored code.
400Bad RequestMissing charge_id/transaction_id; missing or non-numeric amount; amount not greater than 0; amount exceeds the original charge amount; invalid Idempotency-Key.
401UnauthorizedMissing or invalid credentials (Bearer token, Basic auth, or x_login/x_tran_key).
404Not FoundThe referenced charge does not exist within the authenticated merchant scope; unrecognized route.
405Method Not AllowedAny HTTP method other than POST.
409ConflictA request with the same Idempotency-Key is still processing. Includes a Retry-After header.
422Unprocessable EntityThe gateway declined the refund (see transaction.reason_text), or an Idempotency-Key was reused with a different payload.
429Too Many RequestsRate limit exceeded. Includes a Retry-After header and rate limit headers.
500Internal Server ErrorServer-side initialization or processing error.
502Bad GatewayThe upstream payment gateway could not be reached.

Disputes

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

GET /query/disputes List Disputes
List Disputes Query Parameters
Field Type Required Description
pagenumberNoPage number. Defaults to 1.
pageSizenumberNoResults per page, between 1 and 100. Defaults to 25.
sortstringNoSort field, prefixed with - for descending. Supported fields: due_date, posted_date, case_amount, case_status, reason_code, id. Defaults to -due_date.
filtersstringNoStandard filter expression (e.g. filters=case_status~=needs). Applied to the current page only, since disputes are paginated by the upstream system.
modifiersstringNoComma-separated list of fields to include in each record (uniqueID is always included).
List Disputes (Sample Response)
{
  "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.

GET /query/disputes/{id} Retrieve a Dispute

Returns the full dispute record by its uniqueID. Disputes belonging to another merchant return 404.

Retrieve Dispute (Sample Response)
{
  "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": ""
  }
}
POST /query/disputes/{id}/accept Accept a Dispute

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.

Accept Dispute Body Parameters
Field Type Required Description
commentstringNoOptional comment recorded with the response.
x_loginstringNoMerchant login (only if not using Authorization header).
x_tran_keystringNoMerchant tran key/secret (only if not using Authorization header).
POST /query/disputes/{id}/challenge Challenge a Dispute

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).

Challenge Dispute Body Parameters (multipart/form-data)
Field Type Required Description
filefileYes*Evidence document (JPG, PNG, GIF, PDF, or TXT). *Optional only when the dispute already has uploaded documents.
commentstringNoOptional comment recorded with the challenge.
x_loginstringNoMerchant login (only if not using Authorization header).
x_tran_keystringNoMerchant tran key/secret (only if not using Authorization header).
Challenge Dispute (Sample Request)
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"
Respond to Dispute (Sample Response)
{
  "result": "success",
  "message": "Challenge submitted",
  "dispute": {
    "uniqueID": 448821,
    "case_status": "Under Review",
    "item_type": "Resolved",
    "...": "..."
  }
}
Dispute Object Fields

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
uniqueIDnumberDispute identifier. Use with GET /query/disputes/{id}.
case_numberstringCase number assigned by the processor.
case_typestringType of case (e.g. Chargeback, Retrieval, Pre-Arbitration).
case_statusstringCurrent status of the case (e.g. Needs Response, Under Review, Closed).
case_amountnumberDisputed amount.
currencystringCurrency of the disputed amount.
reason_codestringCard-network reason code (e.g. 10.4, 4837).
reason_descriptionstringHuman-readable description of the reason code.
card_brandstringCard network (Visa, Mastercard, etc.).
card_namestringCardholder name, when provided by the network.
cardholder_account_numberstringMasked card number.
auth_codestringAuthorization code of the original transaction.
arnstringAcquirer Reference Number of the original transaction.
order_idstringMerchant order reference, when available.
transaction_datestringDate of the original transaction.
chargeback_datestringDate the chargeback was initiated.
posted_datestringDate the case was posted to your account.
due_datestringDeadline to respond to the case.
item_typestringWorkflow state of the item: Open or Resolved.
notesstringCase notes, when available.
HTTP Status Codes
Code Meaning When it is returned
200OKThe dispute list or dispute was returned, or the response (accept/challenge) was submitted successfully.
400Bad RequestInvalid page/pageSize; unsupported sort field; missing, oversized, or invalid evidence file on a challenge.
401UnauthorizedMissing or invalid credentials (Bearer token, Basic auth, or x_login/x_tran_key).
404Not FoundThe dispute does not exist or does not belong to the authenticated merchant.
405Method Not AllowedAny method/route combination other than the documented GET and POST routes.
409ConflictThe dispute is not open for responses (accept/challenge on a closed or already-answered case).
422Unprocessable EntityThe merchant account does not have a MID configured.
502Bad GatewayThe chargeback processing system could not be reached or returned an error.
503Service UnavailableThe dispute service is not configured for this account.

Voids

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

POST /voids Create a Void
POST /voids/{id} Create a Void (Path Alias)
Create Void Body Parameters
Field Type Required Description
charge_idnumberConditionalOriginal charge Transaction presentation_id. Required unless id is supplied in the URL path. Alias: transaction_id.
notesstringNoOptional notes sent to the gateway.
x_loginstringNoMerchant login (only if not using Authorization header).
x_tran_keystringNoMerchant tran key/secret (only if not using Authorization header).
Create Void (Body)
{
  "charge_id": 123456,
  "notes": "Customer requested cancellation"
}
Create Void (Sample Response)
{
  "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"
  }
}

Webhooks

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

POST /query/webhook Create Webhook Endpoint
GET /query/webhook List Webhook Endpoints
GET /query/webhook/{id} Retrieve Webhook Endpoint
PATCH /query/webhook/{id} Update Webhook Endpoint
DELETE /query/webhook/{id} Delete Webhook Endpoint
Endpoint Body Parameters
Field Type Required Description
urlstringYesHTTPS receiver URL. HTTP is only allowed when WEBHOOK_ALLOW_HTTP=true.
eventsarray/stringNoEvent types to deliver. Use ["*"] for all events.
descriptionstringNoInternal label for the endpoint.
enabledbooleanNoDefaults to true. Disabled endpoints do not receive deliveries.
Create Webhook Endpoint
{
  "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
}
Create Response
{
  "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_..."
  }
}
Supported Events
Event When it fires
charge.createdA charge succeeds through the Charges API.
charge.updatedA charge description or notes field is updated.
charge.capturedAn authorized charge is captured.
charge.failedA charge or capture attempt returns a gateway failure and a transaction-backed charge exists.
refund.createdA refund succeeds through the Refunds API.
recurring.payment_succeededA recurring payment run succeeds.
recurring.payment_failedA recurring payment run fails or has a gateway transport error.
recurring.completedA recurring schedule completes after a successful final payment.
webhook.testA test event sent from the webhook API.
Delivery Format

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.

Event Payload
{
  "id": "evt_...",
  "object": "event",
  "type": "charge.created",
  "created": 1782765600,
  "data": {
    "object": {
      "id": 123456,
      "transaction_id": 98765,
      "amount": 12.34,
      "currency": "USD",
      "status_text": "succeeded"
    }
  }
}
POST /query/webhook/{id}/test Send Test Event
GET /query/webhook/events?limit=25 List Events
GET /query/webhook/deliveries?limit=25 List Delivery Attempts
POST /query/webhook/resend/{delivery_id} Resend Delivery

Customer

Manage customer profiles with a single endpoint. Create customers and store contact, billing, and custom fields for downstream billing workflows.

GET /query/customer Get All Customers
List Query Parameters
Field Type Required Description
pageintegerNoOptional page number for customer list results.
pageSizeintegerNoOptional page size for customer list results.
filtersstringNoOptional filter expression.
sortstringNoOptional sort key (for example -uniqueID).
modifiersstringNoOptional comma-separated response field projection.
List Customers Example
GET /query/customer?page=1&pageSize=25&sort=-uniqueID
GET /query/customer/{id} Get Single Customer
Sample Response
{
  "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"
      }
    ]
  }
}
POST /query/customer Create Customer
Body Parameters
{
  "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"
    }
  }
}
PUT /query/customer/{id} Update Customer
Update Customer Body
{
  "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
      }
    ]
  }
}
DELETE /query/customer/{id} Delete Customer

Recurring

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.

GET /query/recurring Get All Recurring
List Query Parameters
Field Type Required Description
pageintegerNoOptional page number for recurring list results.
pageSizeintegerNoOptional page size for recurring list results.
filtersstringNoOptional filter expression.
sortstringNoOptional sort key (for example -uniqueID).
modifiersstringNoOptional comma-separated response field projection.
List Recurring Example
GET /query/recurring?page=1&pageSize=25&sort=-uniqueID
GET /query/recurring/{id} Get Single Recurring
Sample Response (Get Single Recurring)
{
  "result": "success",
  "data": {
    "uniqueID": 12345,
    "status": 1,
    "interval": 3,
    "interval_number": 1,
    "run_until": 2,
    "run_limit": 12,
    "end_date": null,
    "run_next": "2026-09-25 00:00:00",
    "run_last": "2026-08-25 00:00:00",
    "run_count": 2,
    "run_total": 400.00,
    "amount": 203.50,
    "surcharge": 3.50,
    "currency": "USD",
    "customer_id": 6531,
    "payment_id": 3370,
    "idempotency_key": "recurring-single-20260725231736",
    "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
      }
    ]
  }
}
POST /query/recurring Create Recurring
Parameters
Field Required Notes
customer_idYesCustomer for the schedule.
payment_idYesStored payment method ID that belongs to the provided customer.
statusYesRecurring status value. Allowed: 1=Active, 2=Completed, 4=Paused, 5=Terminated.
surchargeYesNumeric surcharge added to computed recurring amount.
run_transactionYesWhen true, executes Recurring::runRecurring() after save.
idempotency_keyConditionalClient-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_idModeSingle-product mode. Must not be sent together with root products.
productsModeMulti-product mode. Array/object of items (normalized internally). Must not be sent with root product_id.
recurring_rule_productMulti ModeOptional schedule anchor. If provided, it must belong to merchant, be recurring-enabled, and be present in products.
intervalNoOptional override: 1=Day, 2=Week, 3=Month, 4=Year.
interval_numberNoOptional override; must be >= 1.
run_untilNoOptional override: 0=Until terminated, 1=Specific date, 2=Fixed count.
run_limitConditionalRequired when run_until is 2 (count).
end_dateConditionalRequired when run_until is 1. Must be a future date based on the merchant-local date; date/time value is accepted as provided.
start_dateNoDate/time value is accepted as provided. Defaults to current date/time when omitted.
run_lastNoDate/time value is accepted as provided.
run_nextNoMust 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_totalNoOptional decimal, defaults to 0.
run_countNoOptional whole number, defaults to 0.

Validation Notes (Create):

  • Date/time values are passed through by the endpoint and handled by the existing recurring model/database conversion.
  • 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.
  • All referenced products must belong to the same merchant as x_login.
  • In multi-product mode, each item may use product_id (or alias productID). For custom items without a product ID, name and price are required; qty defaults to 1.
  • Schedule resolution in multi-product mode:
    • If recurring_rule_product is provided, its recurring rule is used.
    • If omitted and exactly one recurring-enabled product exists in products, that product's recurring rule is used.
    • If omitted and multiple recurring-enabled products exist, provide recurring_rule_product or explicit schedule fields.
    • If omitted and no recurring-enabled products exist, explicit schedule fields are required.
  • Amount calculation:
    • Single-product mode: amount = product price + surcharge
    • Multi-product mode: amount = sum(qty * unit_price) + surcharge
Sample Body (Single-Product Mode)
{
  "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-20260725231736",
    "run_next": "2026-08-25",
    "interval": 2,
    "interval_number": 1,
    "run_until": 2,
    "run_limit": 12
  }
}
Sample Body (Multi-Product Mode)
{
  "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-20260725231736",
    "interval": 2,
    "interval_number": 1,
    "run_until": 1,
    "end_date": "2027-01-25"
  }
}
Sample Body (Multi-Product Without Recurring Product)
{
  "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
  }
}
PUT /query/recurring/{id} Update Recurring
Parameters
Field Required Description
uniqueIDNoOptional when calling /query/recurring/{id}; otherwise required.
statusNo1=Active, 2=Completed, 4=Paused, 5=Terminated.
intervalNo1=Day, 2=Week, 3=Month, 4=Year.
interval_numberNoNumber of intervals between runs.
run_untilNo0=Until terminated, 1=Specific date, 2=Fixed count.
run_limitNoRequired when run_until is 2 (count).
end_dateNoRequired when run_until is 1; must be a future date based on the merchant-local date when provided.
run_nextNoNext run date/time; must be a future date based on the merchant-local date when provided.
surchargeNoSurcharge amount added to product price.
payment_idNoStored payment method ID.

Note: Recurring line items cannot be updated via the update endpoint.

Sample Update Body
{
  "x_login": "...",
  "x_tran_key": "...",
  "_method": "PUT",
  "recurring": {
    "status": 1,
    "interval": 2,
    "interval_number": 1,
    "run_until": 2,
    "run_limit": 12,
    "run_next": "2026-09-01"
  }
}
DELETE /query/recurring/{id} Delete Recurring
Failed Payment Behavior

When a scheduled recurring payment run fails (card declined or gateway transport error), the schedule itself is not changed:

  • The recurring status remains 1 (Active); the schedule is not paused or terminated automatically.
  • run_next, run_count, and run_total are not advanced. The schedule only advances after a successful payment.
  • The failed run is retried automatically on the next daily billing cycle, and continues to retry once per day until the payment succeeds or the schedule's status is changed (for example to 4=Paused or 5=Terminated via the update endpoint).
  • There is no retry limit. To stop retries, update the schedule's status, or update payment_id to a valid stored payment method so the next retry can succeed.
  • A recurring.payment_failed webhook event is emitted for each failed attempt. When a retry later succeeds, recurring.payment_succeeded is emitted and the schedule advances normally. See the Webhooks documentation for event details.

Invoice

Create and manage invoices.

GET /query/invoice Get All Invoices
List Query Parameters
Field Type Required Description
pageintegerNoOptional page number for invoice list results.
pageSizeintegerNoOptional page size for invoice list results.
filtersstringNoOptional filter expression.
sortstringNoOptional sort key (for example -uniqueID).
modifiersstringNoOptional comma-separated response field projection.
List Invoices Example
GET /query/invoice?page=1&pageSize=25&sort=-uniqueID

Supports optional invoice filters:

Filter Description
customer_idLimit to a specific customer
statusInvoice status (e.g., Pending, Sent, Paid)
due_beforeInvoices due before date (YYYY-MM-DD)
due_afterInvoices due after date (YYYY-MM-DD)
GET /query/invoice/{id} Get Single Invoice

Returns a single invoice by unique ID. If not found, returns 404.

POST /query/invoice Create Invoice

Note: Provide customer_id to associate with an existing customer. If customer_id is omitted, include a customer object to create a new customer.

Create Invoice Body
{
  "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
      }
    ]
  }
}
PUT /query/invoice/{id} Update Invoice
Update Invoice Body
{
  "x_login": "...",
  "x_tran_key": "...",
  "_method": "PUT",
  "invoice": {
    "uniqueID": 456,
    "notes": "Updated notes",
    "status": "Sent"
  }
}
DELETE /query/invoice/{id} Delete Invoice
Delete Example
DELETE /query/invoice/456

Products

Manage products and services.

GET /query/product Get All Products

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.
List Query Parameters
Field Type Required Description
pageintegerNoOptional page number for product list results.
pageSizeintegerNoOptional page size for product list results.
filtersstringNoOptional filter expression.
sortstringNoOptional sort key (for example -uniqueID).
modifiersstringNoOptional comma-separated response field projection.
List Products Example
GET /query/product?page=1&pageSize=25&sort=-uniqueID
GET /query/product/{id} Get Single Product
POST /query/product Create Product
Body Parameters
Field Type Required Description
namestringYesProduct name.
descriptionstringNoProduct description.
pricenumberNoProduct price.
isBookablebooleanNoMarks product as bookable.
durationintegerNoDuration in minutes (for bookable products).
is_recurringbooleanNoEnable 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.
Create Product Body
{
  "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"
    }
  }
}
PUT /query/product/{id} Update Product
Body Parameters
Field Type Required Description
uniqueIDintegerNoOptional when calling /query/product/{id}; otherwise required.
namestringNoProduct name.
descriptionstringNoProduct description.
pricenumberNoProduct price.
isBookablebooleanNoMarks product as bookable.
durationintegerNoDuration in minutes (for bookable products).
is_recurringbooleanNoEnable 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.

Update Product Body
{
  "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"
    }
  }
}

Coupons

Manage discount coupons.

GET /query/coupon Get All 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.
List Query Parameters
Field Type Required Description
pageintegerNoOptional page number for coupon list results.
pageSizeintegerNoOptional page size for coupon list results.
filtersstringNoOptional filter expression.
sortstringNoOptional sort key (for example -uniqueID).
modifiersstringNoOptional comma-separated response field projection.
List Coupons Example
GET /query/coupon?page=1&pageSize=25&sort=-uniqueID
GET /query/coupon/{id} Get Single Coupon
GET /query/coupon?code={code} Get Coupon By Code
POST /query/coupon Create Coupon

Create accepts either a nested coupon object or a flat top-level payload. name is required and cannot be blank.

Body Parameters
Field Type Required Description
namestringYesCoupon name.
descriptionstringNoCoupon description.
codestringNoCoupon code customers enter.
percent_offnumberNoPercent discount (e.g., 10 for 10% off).
duration_typestringNoDiscount duration (e.g., once).
first_purchase_onlybooleanNoOnly apply to the first purchase.
max_redemptionsintegerNoMaximum number of redemptions allowed.
enabledbooleanNoEnable/disable coupon.
Create Coupon Body
{
  "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
  }
}
Create Coupon Body (Flat Payload)
{
  "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
}
PUT /query/coupon/{id} Update Coupon

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.

Body Parameters
Field Type Required Description
uniqueIDintegerNoOptional when calling /query/coupon/{id}; otherwise required.
namestringNoCoupon name.
descriptionstringNoCoupon description.
codestringNoCoupon code customers enter.
percent_offnumberNoPercent discount.
duration_typestringNoDiscount duration.
first_purchase_onlybooleanNoOnly apply to the first purchase.
max_redemptionsintegerNoMaximum number of redemptions allowed.
enabledbooleanNoEnable/disable coupon.
Update Coupon Body
{
  "x_login": "...",
  "x_tran_key": "...",
  "_method": "PUT",
  "coupon": {
    "name": "Updated Coupon Name",
    "percent_off": 15,
    "enabled": true
  }
}
DELETE /query/coupon/{id} Delete Coupon
Delete Coupon Example
DELETE /query/coupon/123

Booking Module

Appointment Scheduling via the Payment Gateway


The Booking Module lets merchants manage appointment-based services, staff, schedules, and customer bookings — all through the payment gateway's standard /query/booking endpoint. The gateway handles authentication, resolves the caller's identity, mints a module JWT, and forwards the request to the booking backend.

Base URL

All booking calls go through: /query/booking/{path}

Request & Response Format

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", ... }
Authentication

Every endpoint except the manage token flows requires the caller to authenticate. The gateway resolves identity in order:

  1. Session — active browser/cookie session for the gateway user.
  2. Bearer token — a gateway-issued JWT passed as Authorization: Bearer <token> with a sub or user_id claim.
  3. Basic AuthAuthorization: Basic base64(merchanttext:secret), the merchant's existing API credential (the same one used for x_login/x_tran_key elsewhere in the gateway).

The resolved user's account type determines the booking permission forwarded to the backend:

  • merchant, enhanced, sysad account types → merchant permission

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.

Public-Scoped Endpoints (still require Basic Auth)

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 slots
  • POST /query/booking with is_public_booking: true in the body — create a booking (public/customer-facing). Omitting the flag (e.g. internal/migration callers using the same merchant credential) resolves to full merchant permission instead.
  • GET /query/booking/staff, GET /query/booking/staff/{staffId}, GET /query/booking/services, and GET /query/booking/merchant with ?is_public=true — top-level reads only (no sub-resource path segment). Omitting the flag resolves to full merchant permission instead. Each endpoint's response is narrowed for public callers — see that endpoint's section below for the exact fields returned.

The /query/booking/manage/{token}/... flows remain fully unauthenticated — they are gated solely by the signed manage token in the URL.


Availability Slots

GET /query/booking/slots Get Available Slots

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.

Query Parameters
FieldTypeRequiredDescription
serviceIdUUIDYesService to evaluate.
staffIdUUIDConditionalRequired when the service uses providerMode = atCreation; must be omitted for later.
datestring YYYY-MM-DDYesLocal calendar date to query.
displayTzstringNoOptional. Return slot times converted into this timezone instead of the merchant's own. Defaults to the merchant's timezone when omitted.
quantityintegerNoNumber of seats. Defaults to 1.
Get Slots Example
GET /query/booking/slots?serviceId=uuid&staffId=uuid&date=2027-06-01&displayTz=America%2FLos_Angeles
Authorization: Basic base64(merchanttext:secret)
Response
{
  "result": "success",
  "slots": [
    { "time": "09:00" },
    { "time": "10:00" },
    { "time": "14:00" }
  ]
}

Bookings

POST /query/booking Create Booking

Creates a booking. Requires the merchant's Basic Auth credential (or an authenticated merchant/staff session). Pass is_public_booking: true in the body for the customer-facing widget flow — this scopes the request to public permission instead of full merchant permission. Omit it for internal/migration callers that need full merchant permission.

Staff callers must have canCreateBooking = true. Set paymentMethod to online when the customer will pay via the payment gateway; the booking is created in a pending state and confirmed later via confirm-payment.

Body Parameters
FieldTypeRequiredDescription
is_public_bookingbooleanNoSet true for customer-widget bookings to scope to public permission. Defaults to false (full merchant permission).
serviceIdUUIDYesService being booked.
staffIdUUIDConditionalRequired for atCreation services; must be omitted for later.
scheduledAtISO 8601 datetimeYesBooking start time (minute precision).
timeZonestringYesTimezone identifier.
paymentMethodinPerson | onlineYesPayment method chosen by the customer.
customerExternalIdintegerYesGateway customer/user ID.
customerFnamestringYesCustomer first name (1–25 chars).
customerLnamestringYesCustomer last name (1–25 chars).
customerEmailstringConditionalRequired when emailRequired is set to true on the merchant.
customerPhonestringYesE.164 phone if provided.
taxRatenumberNoTax percentage (non-negative).
surchargeTypeflat | percentageConditionalRequired together with surchargeAmount.
surchargeAmountnumberConditionalRequired together with surchargeType.
quantityintegerNoSeats/units. Defaults to 1.
notesstringNoInternal notes.
Create Booking Body
{
  "is_public_booking": true,
  "booking": {
    "serviceId":          "uuid",
    "staffId":            "uuid",
    "scheduledAt":        "2027-06-01T09:00Z",
    "timeZone":           "UTC",
    "paymentMethod":      "online",
    "customerExternalId": 5001,
    "customerFname":      "Jane",
    "customerLname":      "Doe",
    "customerEmail":      "jane@example.com",
    "customerPhone":      "+15551234567",
    "taxRate":            8,
    "quantity":           1
  }
}
Response (201)
{
  "result": "success",
  "booking": {
    "id":                      "uuid",
    "serviceId":               "uuid",
    "staffId":                 "uuid",
    "scheduledAt":             "2027-06-01T09:00:00.000Z",
    "endsAt":                  "2027-06-01T10:00:00.000Z",
    "paymentMethod":           "online",
    "paymentStatus":           "pending",
    "status":                  "pending",
    "snapshotPrice":           "30.00",
    "snapshotDurationMinutes": 60,
    "totalPrice":              "30.00",
    "manageToken":             "jwt"
  }
}

GET /query/booking List Bookings

Returns bookings for the authenticated merchant. Staff callers without canViewAllBookings are scoped to their own bookings automatically.

Response
{
  "result": "success",
  "bookings": [
    {
      "id":            "uuid",
      "serviceId":     "uuid",
      "staffId":       "uuid",
      "scheduledAt":   "2027-06-01T09:00:00.000Z",
      "endsAt":        "2027-06-01T10:00:00.000Z",
      "status":        "pending",
      "paymentMethod": "online",
      "paymentStatus": "pending",
      "totalPrice":    "32.40"
    }
  ]
}
Query Parameters
FieldTypeRequiredDescription
datestring YYYY-MM-DDNoFilter by date; must be paired with tz.
tzstringNoTimezone; must be paired with date.
statusenumNopending, confirmed, cancelled, completed, noShow
staffIdUUIDNoFilter by staff. Merchants only; non-admin staff are always scoped to themselves.
serviceIdUUIDNoFilter by service.
customerExternalIdintegerNoFilter by customer.
paymentMethodenumNoinPerson or online
paymentStatusenumNopending, paid, failed, refunded

GET /query/booking/{bookingId} Get Booking

Returns a single booking. Staff callers without canViewAllBookings can only view bookings assigned to themselves.

Response
{
  "result": "success",
  "booking": {
    "id":                      "uuid",
    "serviceId":               "uuid",
    "staffId":                 "uuid",
    "scheduledAt":             "2027-06-01T09:00:00.000Z",
    "endsAt":                  "2027-06-01T10:00:00.000Z",
    "status":                  "pending",
    "paymentMethod":           "online",
    "paymentStatus":           "pending",
    "snapshotName":            "Haircut",
    "snapshotPrice":           "30.00",
    "snapshotDurationMinutes": 60,
    "totalPrice":              "32.40"
  }
}

PATCH /query/booking/{bookingId} Update Booking

Updates a booking. Callable by merchant or staff permission. Staff callers must have canManageBooking = true, and are rejected outright if the request body contains a staffId key at all — not just when it would actually change the value. Only merchant callers may include staffId.

Body Parameters (all optional)
FieldTypeDescription
statusenumconfirmed, completed, noShow, cancelled
paymentStatusenumOnly paid is accepted; only for inPerson bookings.
notesstringFree-text notes.
staffIdUUIDMerchant only — including this key at all gets a staff caller rejected. Must reference an active staff member linked to the service, else 404. See Override Behavior below for what an explicit value does on a later-mode service.
scheduledAtISO 8601 datetimeMust be paired with timeZone.
timeZonestringMust be paired with scheduledAt.
quantityintegerPositive integer.
Update Booking Body
{
  "booking": {
    "status":      "confirmed",
    "notes":       "Arrived 5 min early",
    "scheduledAt": "2027-06-01T10:00Z",
    "timeZone":    "America/New_York",
    "staffId":     "uuid",
    "quantity":    2
  }
}
Response
{
  "result": "success",
  "booking": {
    "id":          "uuid",
    "status":      "confirmed",
    "notes":       "Arrived 5 min early",
    "scheduledAt": "2027-06-01T09:00:00.000Z"
  }
}
Responses
StatusCause
200Booking updated.
400paymentStatus: '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.").
401Missing or invalid token.
403Staff caller lacks canManageBooking; staff caller included a staffId key in the body at all; or the caller's permission isn't merchant/staff.
404Merchant, staff record, or booking not found; or an explicit staffId doesn't reference an active staff member linked to the booking's service.
409Booking 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).
422Body failed schema validation (see the validation-errors note near the bottom of this page).
Override Behavior
  • For 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.
  • For 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.
  • For 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.
  • All slot-unavailability 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.

PATCH /query/booking/{bookingId}/confirm-payment Confirm Payment

Marks an online booking as paid after the payment gateway confirms the transaction. Merchant only. Idempotent when called with the same transaction ID.

Body Parameters
FieldTypeRequiredDescription
transactionExternalIdstringYesGateway transaction identifier.
Confirm Payment Body
{
  "booking": {
    "transactionExternalId": "txn_abc123"
  }
}

PATCH /query/booking/{bookingId}/cancel-payment Cancel Payment

Marks an online payment as failed and cancels the booking. Merchant only. Idempotent when the booking is already in a failed state.


Manage Link (Customer Self-Service)

These endpoints are authenticated solely by the signed manage token returned at booking creation — no gateway session or credentials are needed.

GET /query/booking/manage/{token} Get Booking via Manage Token

Returns the booking addressed by the manage token and flags whether self-service cancel and reschedule are still permitted.

Response
{
  "result": "success",
  "booking": {
    "id":            "uuid",
    "scheduledAt":   "2027-06-01T09:00:00.000Z",
    "status":        "pending",
    "canCancel":     true,
    "canReschedule": true
  }
}

POST /query/booking/manage/{token}/cancel Cancel via Manage Token

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.


PATCH /query/booking/manage/{token}/reschedule Reschedule via Manage Token

Reschedules the booking to a new slot. A new manageToken is returned — the old one is invalidated.

Body Parameters
FieldTypeRequiredDescription
scheduledAtISO 8601 datetimeYesNew start time.
timeZonestringYesTimezone identifier.
Reschedule Body
{
  "booking": {
    "scheduledAt": "2027-06-02T10:00Z",
    "timeZone":    "UTC"
  }
}
Response
{
  "result": "success",
  "booking": {
    "id":          "uuid",
    "scheduledAt": "2027-06-02T10:00:00.000Z",
    "status":      "pending",
    "manageToken": "jwt"
  }
}

Merchant Settings

Merchant-only endpoints. Manage booking settings, weekly opening hours, and one-off date exceptions.

GET /query/booking/merchant Get Merchant Settings

Returns the merchant's booking configuration together with the full weekly schedule and all date exceptions. Accessible by merchant and staff tokens. Also accessible with public permission via ?is_public=true (see Authentication above) — public callers receive a narrowed response with just serviceLabel, slotFormat, emailRequired, timeBeforeBooking, acceptsInPersonPayment, and acceptsOnlinePayment; no schedule, exceptions, or identifying fields.


PUT /query/booking/merchant Upsert Merchant Settings

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.

Body Parameters (all optional)
FieldTypeDefaultDescription
acceptsInPersonPaymentbooleanfalseAccept in-person payments (e.g. cash).
acceptsOnlinePaymentbooleanfalseAccept online payments (e.g. card).
bookingServicebooleanfalseEnable service booking module.
bookingEventbooleanfalseEnable event booking module.
serviceLabelstring""Custom UI label for services.
eventLabelstring""Custom UI label for events.
staffLabelstring""Custom UI label for staff.
timeStepMinutesinteger15Slot granularity in minutes.
timeBeforeBookinginteger1Minimum days in advance a booking can be made.
leadTimeMinutesinteger60Minimum minutes of notice before a booking.
cancelWindowHoursinteger24Hours before scheduledAt when self-cancel/reschedule stop being allowed.
allowStaffPricingOverridesbooleanfalseEnable per-staff price and duration overrides on services.
slotFormattwentyFourHour | 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
emailRequiredbooleanfalseWhen true, POST /api/v1/bookings requires both customerEmail and customerPhone. When false, only customerPhone is required and customerEmail is optional
Upsert Merchant Body
{
  "merchant": {
    "acceptsInPersonPayment":     true,
    "acceptsOnlinePayment":       true,
    "bookingService":             true,
    "serviceLabel":               "Appointment",
    "timeStepMinutes":            30,
    "leadTimeMinutes":            120,
    "cancelWindowHours":          24,
    "allowStaffPricingOverrides": false
  }
}

GET /query/booking/merchant/schedule Get Merchant Schedule

Returns the merchant's weekly opening hours as an array of schedule entries. Returns an empty array if no schedule has been set yet. Accessible by merchant and staff tokens.

Response
{
  "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" }
  ]
}

PUT /query/booking/merchant/schedule Set Merchant Schedule

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.

Body — Array of 7 day entries
FieldTypeRequiredDescription
dayOfWeekinteger 0–6Yes0 = Sunday, 6 = Saturday.
isOpenbooleanYesWhether the merchant is open that day.
openTimestring HH:MMConditionalRequired when isOpen is true.
closeTimestring HH:MMConditionalRequired when isOpen is true.
Set Schedule Body
{
  "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 }
  ]
}

GET /query/booking/merchant/exceptions List Schedule Exceptions

Returns all date exceptions for the merchant. Returns an empty array if none have been created. Accessible by merchant and staff tokens.

Response
{
  "result": "success",
  "exceptions": [
    { "id": "uuid", "date": "2026-12-25", "isOpen": false, "openTime": null, "closeTime": null }
  ]
}

GET /query/booking/merchant/exceptions/{exceptionId} Get Schedule Exception

Returns a single date exception by ID. Accessible by merchant and staff tokens.


PUT /query/booking/merchant/exceptions Upsert Schedule Exception

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.

Body Parameters
FieldTypeRequiredDescription
datestring YYYY-MM-DDYesThe date to override.
isOpenbooleanYesfalse = closed all day. true = open with special hours.
openTimestring HH:MMConditionalRequired when isOpen is true.
closeTimestring HH:MMConditionalRequired when isOpen is true.

DELETE /query/booking/merchant/exceptions/{exceptionId} Delete Schedule Exception

Removes a date exception. Only the owning merchant can delete it.


Services

Services are the bookable items in the catalog. Merchants and authorized staff can create, update, and delete services. Staff access is governed by per-staff permission flags.

POST /query/booking/services Create Service

Creates a service record. Merchant and staff callers with canCreateService = true may call this endpoint.

Body Parameters
FieldTypeRequiredDescription
namestringYesService/product name.
pricenumberYesService price (positive).
descriptionstringNoService description.
durationMinutesintegerYesHow long the service takes (positive integer).
capacityintegerNoMax simultaneous bookings per slot. Omit for unlimited.
capacityModeperStaff | perServiceNoDefaults to perStaff. Whether capacity is enforced per staff member or shared across all staff for the slot. Only meaningful when capacity is set.
mondaysundaybooleanNoDays of the week when the service is offered. All default to false.
schedulingModeblocks | startTimeNoDefaults to blocks. Use startTime for a fixed daily start time.
fixedStartTimestring HH:MMConditionalRequired when schedulingMode is startTime.
providerModeatCreation | laterNoDefaults to atCreation. later defers staff assignment after booking.
Create Service Body
{
  "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"
  }
}

GET /query/booking/services List Services

Returns all services for the merchant. Staff callers with canViewAllServices = false only see services they are assigned to. Also accessible with public permission via ?is_public=true — public callers always receive all merchant services (no assignment scoping), with each service's staff array reduced to { id, firstName, lastName, color } plus effectivePrice/effectiveDurationMinutes (the override value when one applies, otherwise the service's base price/durationMinutes) in place of the raw priceOverride/durationMinutesOverride fields.


GET /query/booking/services/{serviceId} Get Service

Returns a single service with its assigned staff. Staff callers without canViewAllServices receive 404 if they are not assigned to the requested service.


PATCH /query/booking/services/{serviceId} Update Service

Updates a service. Staff require canEditService = true. Staff without canViewAllServices can only edit services they are assigned to.

Body Parameters (all optional)
FieldTypeDescription
namestringService name.
pricenumberService price (positive).
descriptionstringService description.
durationMinutesintegerDuration in minutes (positive integer).
capacityintegerPositive integer. Send empty string to revert to unlimited.
capacityModeperStaff | perServiceWhether capacity is enforced per staff member or shared across all staff for the slot. Only meaningful when capacity is set.
mondaysundaybooleanDays the service is offered.
schedulingModeblocks | startTime
fixedStartTimestring HH:MMRequired when switching to startTime mode. Cleared automatically when switching back to blocks.
providerModeatCreation | later
Update Service Body
{
  "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"
  }
}

DELETE /query/booking/services/{serviceId} Delete Service

Permanently deletes a service. Staff require canDeleteService = true. Staff without canViewAllServices can only delete services they are assigned to.


PUT /query/booking/services/{serviceId}/staff Set Service Staff Assignments

Replaces the full list of staff assigned to a service. Sending an empty array removes all staff. Merchant only.

Body Parameters
FieldTypeRequiredDescription
staffIdsUUID[]YesArray of staff UUIDs to assign. All must belong to the merchant.
Set Service Staff Body
{
  "staffService": {
    "staffIds": ["uuid-1", "uuid-2"]
  }
}
Response
{
  "result": "success",
  "staffServices": [
    { "staffId": "uuid-1", "serviceId": "uuid", "priceOverride": null, "durationMinutesOverride": null },
    { "staffId": "uuid-2", "serviceId": "uuid", "priceOverride": null, "durationMinutesOverride": null }
  ]
}

GET /query/booking/services/{serviceId}/staff/{staffId} Get Staff Override for Service

Returns the assignment row for one staff member on one service, including current price and duration overrides. Staff callers can only fetch their own row. For the reverse lookup — all services a given staff member is assigned to — see GET /query/booking/staff/{staffId}/services in the Staff section.

Response
{
  "result": "success",
  "staffService": {
    "staffId":                 "uuid",
    "serviceId":               "uuid",
    "priceOverride":           "49.99",
    "durationMinutesOverride": 30
  }
}

PATCH /query/booking/services/{serviceId}/staff/{staffId} Update Staff Override for Service

Sets or clears per-staff price and duration overrides for a service assignment. Requires merchant allowStaffPricingOverrides = true. Staff callers must have canManageOwnPricing = true and can only patch their own row. Send null to clear an override.

Body Parameters (all optional)
FieldTypeDescription
priceOverridenumber | nullOverride price for this staff member. null clears it.
durationMinutesOverrideinteger | nullOverride duration in minutes. null clears it.
Update Staff Override Body
{
  "staffService": {
    "priceOverride":            49.99,
    "durationMinutesOverride":  45
  }
}

Staff

Manage staff members under the merchant, their schedules, recurring breaks, and one-off date exceptions.

POST /query/booking/staff Create Staff Member

Creates a new staff member. Merchant only.

Body Parameters
FieldTypeRequiredDescription
firstNamestringYes1–100 characters.
lastNamestringYes1–100 characters.
emailstringNoValid email address.
userExternalIdintegerNoGateway user ID. Links this staff record to a gateway user account so the user can authenticate as staff.
colorstringNoHex color for UI display. Defaults to #6083b4.
isActivebooleanNoWhether the staff member is active. Defaults to true. Inactive staff cannot be assigned to new bookings.
canManageSchedulebooleanNoStaff can edit their own schedule.
canManageBreaksbooleanNoStaff can edit their own breaks.
canManageExceptionsbooleanNoStaff can edit their own date exceptions.
canCreateServicebooleanNoStaff can create services.
canEditServicebooleanNoStaff can edit assigned services.
canDeleteServicebooleanNoStaff can delete assigned services.
canViewAllServicesbooleanNoStaff can view all services; otherwise only assigned ones.
canViewAllBookingsbooleanNoStaff can view all bookings; otherwise only their own.
canCreateBookingbooleanNoStaff can create bookings on behalf of customers.
canManageBookingbooleanNoStaff can update booking status and notes.
canManageOwnPricingbooleanNoStaff can set their own price/duration overrides (requires merchant allowStaffPricingOverrides).
Create Staff Body
{
  "staff": {
    "firstName":          "Jane",
    "lastName":           "Smith",
    "email":              "jane@example.com",
    "userExternalId":     101,
    "color":              "#FF5733",
    "canCreateBooking":   true,
    "canManageBooking":   true,
    "canViewAllBookings": true
  }
}

GET /query/booking/staff List Staff

Returns all staff under the merchant. Merchant tokens receive full records; staff tokens receive only { id, firstName, lastName, email, color } per entry. Also accessible with public permission via ?is_public=true — public callers receive each entry reduced further to { id, firstName, lastName, color } (no email).


GET /query/booking/staff/{staffId} Get Staff Member

Returns a staff member. Merchant tokens get the full record including all permission flags. Staff tokens get a slim record with { id, firstName, lastName, email, color } only. Also accessible with public permission via ?is_public=true — public callers receive { id, firstName, lastName, color } only (no email, no sub-resources, no permission flags).


PATCH /query/booking/staff/{staffId} Update Staff Member

Updates a staff record. Merchants can update all fields including permission flags and isActive. Staff can only update their own identity fields (firstName, lastName, email, color).

Body Parameters — Merchant (all optional)
FieldTypeDescription
firstNamestring1–100 characters.
lastNamestring1–100 characters.
emailstringValid email address.
userExternalIdintegerPositive integer.
colorstringValid hex color.
isActivebooleanSet to false to deactivate the staff member. Inactive staff cannot be assigned to new bookings.
canManageScheduleboolean
canManageBreaksboolean
canManageExceptionsboolean
canCreateServiceboolean
canEditServiceboolean
canDeleteServiceboolean
canViewAllServicesboolean
canViewAllBookingsboolean
canCreateBookingboolean
canManageBookingboolean
canManageOwnPricingboolean
Body Parameters — Staff (all optional)

Staff callers can only update firstName, lastName, email, and color. Any other field results in 422.

Update Staff Body (Merchant)
{
  "staff": {
    "firstName":          "Jane",
    "lastName":           "Doe",
    "email":              "jane@example.com",
    "color":              "#FF5733",
    "isActive":           true,
    "canManageSchedule":  true,
    "canManageBreaks":    true,
    "canCreateBooking":   true,
    "canManageBooking":   true,
    "canViewAllBookings": false,
    "canCreateService":   false,
    "canEditService":     false,
    "canDeleteService":   false,
    "canViewAllServices": false,
    "canManageOwnPricing": false
  }
}
Update Staff Body (Staff)
{
  "staff": {
    "firstName": "Jane",
    "lastName":  "Doe",
    "email":     "jane@example.com",
    "color":     "#FF5733"
  }
}

DELETE /query/booking/staff/{staffId} Delete Staff Member

Permanently deletes a staff member and all their sub-resources (schedules, breaks, exceptions). Merchant only.


GET /query/booking/staff/{staffId}/services Get Staff Assigned Services

Returns the services this staff member is assigned to, via the staff/service join. Each entry is a full service record plus that staff member's priceOverride and durationMinutesOverride (both null if unset). Returns an empty array if the staff member has no assignments. This is the reverse lookup of GET /query/booking/services/{serviceId}/staff/{staffId}. Staff tokens may only read their own assigned services.

Response
{
  "result": "success",
  "services": [
    {
      "id":                      "uuid",
      "merchantId":              "uuid",
      "name":                    "Haircut",
      "price":                   "50",
      "durationMinutes":         30,
      "priceOverride":           null,
      "durationMinutesOverride": null
    }
  ]
}

GET /query/booking/staff/{staffId}/schedule Get Staff Schedule

Returns the staff member's weekly work schedule. Returns an empty array if no schedule has been set yet. Staff tokens may only read their own schedule.

Response
{
  "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" }
  ]
}

PUT /query/booking/staff/{staffId}/schedule Set Staff Schedule

Replaces the staff member's full weekly work schedule. Must send all 7 days. Staff require canManageSchedule = true.

Body — Array of 7 day entries
FieldTypeRequiredDescription
dayOfWeekinteger 0–6Yes0 = Sunday, 6 = Saturday.
isActivebooleanYesWhether the staff member works that day.
startTimestring HH:MMConditionalRequired when isActive is true.
endTimestring HH:MMConditionalRequired when isActive is true. Must be after startTime.
Set Staff Schedule Body
{
  "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 }
  ]
}

GET /query/booking/staff/{staffId}/breaks List Staff Breaks

Returns all recurring breaks for the staff member. Returns an empty array if none have been created. Staff tokens may only read their own breaks.

Response
{
  "result": "success",
  "breaks": [
    { "id": "uuid", "staffId": "uuid", "dayOfWeek": 1, "startTime": "12:00", "endTime": "13:00", "isActive": true }
  ]
}

GET /query/booking/staff/{staffId}/breaks/{breakId} Get Staff Break

Returns a single break by ID. The break must belong to the specified staff member. Staff tokens may only read their own breaks.


POST /query/booking/staff/{staffId}/breaks Add Staff Break

Adds a recurring break window to a specific day of the week. Staff require canManageBreaks = true.

Body Parameters
FieldTypeRequiredDescription
dayOfWeekinteger 0–6YesDay the break applies to.
startTimestring HH:MMYesBreak start. Must be before endTime.
endTimestring HH:MMYesBreak end. Must be after startTime.
isActivebooleanNoDefaults to true.

PATCH /query/booking/staff/{staffId}/breaks/{breakId} Update Staff Break

Updates one or more fields on an existing break. Staff require canManageBreaks = true. All fields optional. When only one of startTime or endTime is sent, the other is read from the database to validate ordering.

Body Parameters (all optional)
FieldTypeDescription
dayOfWeekinteger 0–6Day the break applies to.
startTimestring HH:MMBreak start time.
endTimestring HH:MMBreak end time. Must be after startTime.
isActivebooleanEnable or disable this break without deleting it.
Update Staff Break Body
{
  "break": {
    "dayOfWeek": 1,
    "startTime": "12:00",
    "endTime":   "13:00",
    "isActive":  true
  }
}

DELETE /query/booking/staff/{staffId}/breaks/{breakId} Delete Staff Break

Removes a recurring break from a staff member. Staff require canManageBreaks = true.


GET /query/booking/staff/{staffId}/exceptions List Staff Date Exceptions

Returns all date exceptions for the staff member. Returns an empty array if none have been created. Staff tokens may only read their own exceptions.

Response
{
  "result": "success",
  "exceptions": [
    { "id": "uuid", "staffId": "uuid", "date": "2026-12-25", "isAvailable": false, "startTime": null, "endTime": null, "breaks": [] }
  ]
}

GET /query/booking/staff/{staffId}/exceptions/{exceptionId} Get Staff Date Exception

Returns a single date exception by ID. The exception must belong to the specified staff member. Staff tokens may only read their own exceptions.


PUT /query/booking/staff/{staffId}/exceptions Upsert Staff Date Exception

Adds or updates a one-off availability override for a staff member (day off or special hours). Idempotent on the same date — calling again with the same date updates the existing record, including replacing its breaks (see below). Staff require canManageExceptions = true.

An exception may also carry breaks: non-repeating break windows scoped to that single date. Exception breaks are additive to the staff member's recurring breaks for that day of week — creating or updating an exception to change hours/availability does not drop the recurring breaks that would otherwise apply. Use breaks when a specific date needs an extra break on top of the usual ones (e.g. a one-off appointment). breaks is only allowed when isAvailable is true, and each PUT fully replaces the exception's existing breaks — omit the field to clear them.

Body Parameters
FieldTypeRequiredDescription
datestring YYYY-MM-DDYesThe date to override.
isAvailablebooleanYesfalse = day off. true = working special hours.
startTimestring HH:MMConditionalRequired when isAvailable is true.
endTimestring HH:MMConditionalRequired when isAvailable is true. Must be after startTime.
breaksarrayNoOnly allowed when isAvailable is true. Fully replaces the exception's existing breaks.
breaks[].startTimestring HH:MMYes (per entry)Must be before endTime.
breaks[].endTimestring HH:MMYes (per entry)Must be after startTime.
breaks[].isActivebooleanNoDefaults to true.
Staff Exception Examples
// 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" }
    ]
  }
}

DELETE /query/booking/staff/{staffId}/exceptions/{exceptionId} Delete Staff Date Exception

Removes a one-off date exception for a staff member. Staff require canManageExceptions = true.


Organizations

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:

  • Bearer tokenAuthorization: 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.

Organization Object
{
  "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.

GET /query/organization Get All Organizations

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.
GET /query/organization/{id} Get Single Organization

Returns 404 if the organization doesn't exist, is disabled, or the authenticated user doesn't have access to it.

POST /query/organization Create Organization

The authenticated user becomes the organization's owner. System administrator accounts cannot create organizations.

Body Parameters
Field Type Required Description
namestringYesOrganization name (minimum 2 characters).
org_keystringNoPublic slug used to look up the organization (e.g. via GET /query/organization/merchants). 2-64 letters, numbers, dashes, or underscores; must be unique.
notesstringNoFree-form notes.
Create Organization Body
{
  "x_login": "...",
  "x_tran_key": "...",
  "organization": {
    "name": "Acme Salons",
    "org_key": "acme-salons",
    "notes": "Rolled up under the Acme parent account"
  }
}
PATCH /query/organization/{id} Update Organization

Only the organization's owner or an admin member may update it. Omit fields you don't want to change.

Body Parameters
Field Type Required Description
namestringNoOrganization name (minimum 2 characters).
org_keystringNoPublic slug; must remain unique. Send an empty string to clear it.
notesstringNoFree-form notes.
Update Organization Body
{
  "x_login": "...",
  "x_tran_key": "...",
  "organization": {
    "name": "Acme Salons & Spa",
    "notes": "Updated after the spa line launch"
  }
}
DELETE /query/organization/{id} Delete Organization

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.


GET /query/organization/merchants List Merchants in an Organization

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.

Query Parameters
Field Type Required Description
org_keystringYesThe organization's public slug.
List Merchants Example
GET /query/organization/merchants?org_key=acme-salons
List Merchants Response
{
  "result": "success",
  "merchants": [
    { "merchant_id": 1024 },
    { "merchant_id": 1031 }
  ]
}

Card Issuing

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:

  • Primary Account: Operates on subaccounts using Primary Account API credentials.
  • User: Cardholder actions using Primary Account API credentials, plus cardholder username/password in body.
Primary Account Endpoints
GET /query/expicard List Cards (Primary account)

Optional filters: search, type (virtual|physical), status.

List Cards Example
GET /query/expicard
GET /query/expicard?uniqueID={CARD_ID} Get Card Details (Primary account)

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 Card Details Example
GET /query/expicard?uniqueID=CARD-12345
GET /query/expicard?subaction=summary&uniqueID={CARD_ID} Get Card Summary (Primary account)

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 Card Summary Example
GET /query/expicard?subaction=summary&uniqueID=CARD-12345
POST /query Issue Card (Primary account)

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.

Issue Card Body
{
  "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"
}
POST /query Fund Card (Primary account)
Fund Card Body
{
  "action": "expicard",
  "subaction": "fund",
  "uniqueID": "CARD-67890",
  "amount": 50.00
}
GET /query/expicard?subaction=transactions&uniqueID={CARD_ID} Get Transactions (Primary account)

Optional filters supported: from, to, status.

Transactions Example
GET /query/expicard?subaction=transactions&uniqueID=CARD-12345&from=2025-10-01&to=2025-10-31&status=approved
POST /query Create Funding Bank (Subaccount)

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.

Create Funding Bank Body
{
  "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.

User Endpoints

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.

GET /query/expicard?role=user&subaction=user_cards List Cards (User)

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.

User Auth Header
Authorization: Basic <base64(cardholder_username:cardholder_password)>
User Body
{
  "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.