Merchant360 API
Build payments and billing into your own app. Take card payments, run ACH debits/credits, refund, and drive invoices, estimates, customers, catalog, subscriptions, appointments, classes, and more – with scoped, rate-limited API keys, idempotent writes, signed webhooks, and a live sandbox. Every request runs against the merchant’s live Merchant360 account through the same engine as the app, so behavior matches the product exactly.
Introduction
JSON over HTTPS. All amounts are integers in cents unless noted. Timestamps are ISO 8601 (UTC). Base URL:
https://www.merchant360.net/api/m360/v1Every write accepts an optional Idempotency-Key header, and every list is paginated. Object responses include an object field naming the type. A machine-readable OpenAPI 3.1 spec lives at GET https://www.merchant360.net/api/m360/v1/openapi.json.
▶ See every tool working live on the examples page.
Build against the payments, vault, and hosted-fields APIs in test mode – no merchant account needed.
Open the developer sandbox →Authentication
Create a key in Merchant360 under Developers & API (owner only). Send it as a bearer token on every request:
Authorization: Bearer m360_live_xxxKeys are per-merchant, scoped, and rate-limited. Missing scope → 403. GET /me echoes the key’s merchant, scopes, and mode.
No merchant yet? Grab a self-serve test key from the sandbox to build against payments, vault, and hosted fields in test mode.
curl -X GET https://www.merchant360.net/api/m360/v1/me \ -H "Authorization: Bearer m360_live_xxx"
Scopes
Grant a key only what it needs. :read permits GET, :write permits create/update/delete.
| Field | Type | Description |
|---|---|---|
payments:read / :writeoptional | scope | Charge cards, refund, read transactions. |
cards:read / :writeoptional | scope | Card vault (tokens) + hosted-field sessions. |
checkout:read / :writeoptional | scope | Hosted Checkout Sessions. |
ach:writeoptional | scope | ACH debits and credits. |
banks:read / :writeoptional | scope | Saved customer bank accounts (the ACH vault). |
payment_links:read / :writeoptional | scope | Hosted payment links. |
customers:read / :writeoptional | scope | Customer CRM. |
invoices:read / :writeoptional | scope | Invoices (create, send, void). |
estimates:read / :writeoptional | scope | Estimates. |
credit_memos:read / :writeoptional | scope | Credit memos. |
inventory:read / :writeoptional | scope | Catalog products + stock. |
tax_rates:read / :writeoptional | scope | Tax table. |
recurring:read / :writeoptional | scope | Subscriptions. |
scheduling:read / :writeoptional | scope | Appointments, service types, classes + class bookings. |
reports:readoptional | scope | Report summaries. |
bnpl:read / :writeoptional | scope | Buy Now, Pay Later (Affirm + Klarna): sessions, charges, refunds. |
vendor_payments:read / :writeoptional | scope | Accounts payable: payees, recorded payments, 1099/W-9. |
settings:writeoptional | scope | Update accepted methods + installments (read is open on any key). |
statements:readoptional | scope | Download the merchant's processing statement PDF by period. |
webhooks:manageoptional | scope | Register/list/delete webhook endpoints. |
Test mode
Keys prefixed m360_test_ run in sandbox: requests are validated and shaped exactly like live – payments, tokens, hosted fields, Checkout Sessions, and ACH all simulate end-to-end – but no money moves and no gateway/bank call is made. Test-mode activity is kept separate from the merchant’s live Transactions. Switch to a m360_live_ key to move real money.
Test cards. Use any future expiry and any CVV. Unknown card numbers approve with a full AVS match; these exercise each decline and AVS path (declines return 402 with decline_code; test approvals carry avs_result):
| Card number | Outcome | Code |
|---|---|---|
| 4242 4242 4242 4242 | Approved (AVS: address + ZIP match) | 00 / Y |
| 4000 0000 0000 0010 | Approved – AVS: address matches, ZIP does not | 00 / A |
| 4000 0000 0000 0028 | Approved – AVS: ZIP matches, address does not | 00 / Z |
| 4000 0000 0000 0036 | Approved – AVS: no match | 00 / N |
| 4000 0000 0000 0002 | Declined – do not honor | 05 |
| 4000 0000 0000 9995 | Declined – insufficient funds | 51 |
| 4000 0000 0000 0069 | Declined – expired card | 54 |
| 4000 0000 0000 0127 | Declined – incorrect CVV | 82 |
The same numbers work on every test-mode surface – the API, hosted Checkout Sessions, and plugin test connections. A card_token containing the word decline also simulates a decline.
Test objects are unmistakable by id prefix – test_txn_ (payments), test_auth_ (authorizations), test_ach_ / ach_test (ACH), card_test_ (saved cards), test_re_ (refunds).
Errors
Every error – validation, auth, decline, rate limit – comes back in the same envelope: { error: { message, code } }. The code is a stable machine-readable string that derives from the HTTP status (400 → bad_request, 401 → unauthorized, 402 → payment_declined, and so on), so you can branch on either the status or the code. message is human-readable; log it, don’t parse it.
| Field | Type | Description |
|---|---|---|
400 bad_requestoptional | status | Malformed or missing parameters. |
401 unauthorizedoptional | status | Missing/invalid key, or not a Merchant360 key. |
402 payment_declinedoptional | status | The card or refund was declined. |
403 insufficient_scopeoptional | status | The key lacks the required scope. |
404 not_foundoptional | status | No such object for this merchant. |
409 conflictoptional | status | Idempotency-Key in progress, or a booking clash. |
422 unprocessableoptional | status | Valid shape but rejected by policy (limits, state). |
429 rate_limitedoptional | status | Per-key rate limit exceeded. |
A declined card is an error, not a status: POST /payments returns 402 payment_declined with a decline_code saying why, and no payment object is created. Some 4xx responses carry a more specific code than the table above (e.g. 422 idempotency_mismatch, 409 already_settled) – those are called out on the endpoints that raise them.
{
"error": {
"message": "Your card was declined.",
"code": "payment_declined"
}
}Idempotency
Send an Idempotency-Key header (any unique string, e.g. a UUID) on any POST. The request runs once per (merchant, key): once the original finishes, a retry with the same key replays the stored response instead of acting again. While the first attempt is still in flight, a concurrent duplicate returns 409 idempotency_in_progress; back off briefly and retry to receive the stored result. Reusing a key against a different method or path returns 422 idempotency_mismatch – mint a fresh key per logical operation.
Idempotency-Key: 8f14e45f-cea1-4a3d-9f2e-1b2c3d4e5f60Pagination
| Field | Type | Description |
|---|---|---|
pageoptional | integer | 1-based page number. Default 1. |
page_sizeoptional | integer | Items per page, 1–100. Default 25. |
{ "data": [ … ], "page": 1, "page_size": 25, "total": 132, "has_more": true }Payments
/paymentsCharge a card. Returns 201, or 402 on decline./paymentsList recent payments./accounting/record-paymentWrite-back: post the payment into the merchant's connected QuickBooks/Xero, applied to THEIR invoice by number. reference = your txn id (also the duplicate guard). GET /settings tells you which provider is connected (accounting.provider) and whether the merchant allows unattended recording (accounting.auto_record_from_extension). Send auto:true when no human confirmed the invoice number – that path requires the opt-in and refuses already-paid invoices instead of double-posting./servicetitan/record-paymentWrite-back for a charge taken against a ServiceTitan invoice: resolves the invoice number to its ServiceTitan id and posts a payment split onto it. Same body + auto semantics as the accounting write-back; GET /settings reports servicetitan.connected + servicetitan.auto_record_from_extension. Use THIS instead of /accounting/record-payment for ServiceTitan charges – ServiceTitan owns the invoice and its own accounting export carries the payment downstream, so writing to both would record it twice.Card payments are synchronous – the response you get back is the final answer, and there is nothing to poll. A successful charge returns 201 with status: "succeeded"; the funds are committed to the merchant’s next settlement batch. A decline is not a status: it surfaces as a 402 error with code: "payment_declined" and a decline_code telling you why, and no payment object is created. On GET /payments, status is the derived succeeded or failed, plus a settled boolean that flips once the batch settles.
You don’t need webhooks to confirm a card charge – but payment.succeeded fires on every successful sale (and card.created when save_card vaults the card), which is the clean feed for reconciliation and for systems that don’t sit on the request path.
Origin attribution. Sales keyed on a Merchant360 terminal carry an origin on the payment object – so software embedding the Embedded Terminal can pull ONLY its own sales and ignore activity that originated elsewhere. Values:
| Field | Type | Description |
|---|---|---|
EMBED_VToptional | string | Keyed on YOUR embedded terminal (embed_key_id names the embed key that carried the session). |
EMBED_HPoptional | string | Pushed to a physical Handpoint device from your embedded terminal. |
M360_VToptional | string | Keyed on the in-app Merchant360 virtual terminal. |
M360_HPoptional | string | Pushed to a Handpoint device from the in-app hardware terminal. |
ISO_VToptional | string | Keyed on the sales office's ISO360 terminal view of this account. |
GET /payments accepts ?origin=EMBED_VT and ?embed_key=<key id> filters, and every row serializes origin + embed_key_id(null on flows that aren’t terminal-keyed – invoices, pay links, API charges – which identify by their own objects/events instead).
Present the card ONE of three ways: a keyed card, a saved card_token (a card on file – see Cards), or a card-present card_present capture from a terminal or mobile SDK. Exactly one is required.
Card-present (preview). card_present carries the EMV kernel output – emv_data is the DE 55 ICC TLV payload (hex), track_2 the chip’s track-2 image, ksn the DUKPT Key Serial Number for encrypting readers – and M360 forwards it to the Cygma host as ICCSystemRelatedData / Track2Data / KeySerialNumber with the matching POSEntryMode. Test keys simulate approvals today; live keys return card_present_unavailable (503) until EMV certification and the P2PE decryption key are in place – contact EPI to join the pilot.
| Field | Type | Description |
|---|---|---|
amountrequired | integer | Amount to charge, in cents (> 0). |
currencyoptional | string | Only usd (default). |
card_tokenoptional | string | Charge a saved card on file. Use INSTEAD of card. From save_card or POST /tokens. |
card_present.entry_modeoptional | string | Card-present read type: chip, contactless, emv_fallback, swipe, or track1. Preview. |
card_present.emv_dataoptional | string | DE 55 ICC TLV payload (hex) from the EMV kernel. Required for chip / contactless / emv_fallback. |
card_present.track_2optional | string | Track-2 data (raw swipe, or the chip's track-2 image on an EMV read). |
card_present.track_1optional | string | Track-1 data (track1 entry mode). |
card_present.ksnoptional | string | DUKPT Key Serial Number when the capture device encrypts (P2PE). |
card.numberoptional | string | PAN, digits (spaces ok). Required unless card_token or card_present is given. |
card.exp_monthoptional | string | Two digits, 01–12. Required with card. |
card.exp_yearoptional | string | Two or four digits (27 or 2027). Required with card. |
card.cvvoptional | string | Security code. |
cardholder.nameoptional | string | Full name; split into first/last. |
cardholder.emailoptional | string | Receipt/AVS. |
cardholder.phoneoptional | string | Contact. |
cardholder.zipoptional | string | Billing ZIP (AVS). |
cardholder.addressoptional | string | Billing street (AVS). |
sales_taxrequired to qualify | integer | Tax portion in cents. Activates Level II – see Level II & III. |
invoice_numberoptional | string | Your invoice / order reference, max 120 chars. Sent to the processor as the invoice reference and echoed back on the payment. Supersedes description, which still works. |
customer_codeoptional | string | Your identifier for the customer, max 64 chars. Stored with the payment and echoed back, so you can reconcile without keeping a side table. |
line_itemsrequired to qualify | array | Level III line-item detail, max 15 items. Full field reference, tier requirements, and a complete example: Level II & III. |
freightoptional | integer | Total freight / shipping cents included in amount (Level III header field). |
descriptionoptional | string | Reference shown on the transaction (≤120 chars). Also used as the Level II/III merchant order reference. |
save_cardoptional | boolean | Vault the keyed card and return a reusable card_token for future charges/subscriptions. |
surchargeoptional | string | Keyed charges only. "auto" applies the merchant’s surcharge program server-side: BIN-gated to CREDIT cards, capped at 3%, and auto-dropped (clean resubmit) if the network refuses the fee. amount stays the BASE owed; the response’s amount is the total charged, itemized via amount_base / amount_surcharge / surcharge_pct. Card-brand rules require disclosing the fee before charging – quote it first (below). |
| Field | Type | Description |
|---|---|---|
idoptional | string | Payment id. Use it for refunds, voids, and lookups. |
statusoptional | string | succeeded on approval. A decline returns HTTP 402 with an error envelope, not this object. |
amountoptional | integer | Total charged in cents. With surcharge this is the total; amount_base and amount_surcharge ride alongside. |
card.last4optional | string | Last four of the PAN used. |
invoice_numberoptional | string | Echo of what you sent (or of the legacy description field). Null when neither was supplied. |
customer_codeoptional | string | Echo of what you sent. Null when not supplied. |
processor.auth_codeoptional | string | Issuer approval code. This is the number to print on a receipt and to quote when disputing. |
processor.response_codeoptional | string | Raw network response code (00 approved, 10 partial approval). |
processor.response_messageoptional | string | Human-readable result from the host. Log it; don't parse it. |
processor.avs_resultoptional | string | Address-verification verdict. Common values: Y address and ZIP match, Z ZIP only, A address only, N neither, U unavailable. |
processor.cvv_resultoptional | string | Security-code verdict: M match, N no match, P not processed, S should be present but was not, U issuer unavailable. |
processor.rrnoptional | string | Retrieval reference number. The strongest key for locating this transaction with the processor later. |
card_tokenoptional | string | Saved-card token when save_card or card_token was used. vault_token is a back-compat alias. |
metadataoptional | object | Your key/value metadata, echoed back. |
modeoptional | string | live or test. |
The response returns the saved card as card_token; vault_token is kept as a back-compat alias for the same value. With surcharge: "auto" no reusable token is returned – vault the card via POST /tokens when you need both. surcharge: "auto" composes with line_items – a surcharged sale still carries the Level III packet.
| Field | Type | Description |
|---|---|---|
page / page_sizeoptional | integer | Pagination. |
viaoptional | string | Filter to payments whose metadata via equals this value – e.g. the Payment Extension lists only its own charges with via=payment-extension. |
| Field | Type | Description |
|---|---|---|
amountrequired | integer | Base cents owed. |
binrequired | string | The card’s first 6–8 digits – never send a full PAN here. Returns { surcharge_cents, total_cents, pct, card_type, brand } using the exact logic surcharge: "auto" applies at charge time, so the quote and the charge always agree. Show the itemized fee to the cardholder before you call POST /payments. |
curl -X POST https://www.merchant360.net/api/m360/v1/payments \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 2500,
"card": {
"number": "4242424242424242",
"exp_month": "12",
"exp_year": "2027",
"cvv": "123"
},
"cardholder": {
"name": "Jane Doe",
"zip": "90210",
"email": "jane@example.com"
},
"description": "Order #1042",
"save_card": true
}'Level II & Level III
Card networks price commercial cards (business / corporate / purchasing) by how much transaction data is submitted – the three data levels below. You send the business data on POST /payments; the gateway assembles and transmits the full processor packet. Nothing else changes about the call.
| Field | Type | Description |
|---|---|---|
Level Ioptional | data level | A plain charge – amount + card. No additional data submitted. |
Level IIoptional | data level | Add sales_tax (the tax portion of amount, in cents) and a description. The gateway completes the rest of the Level II data set automatically. |
Level IIIoptional | data level | Add line_items (with commodity codes) + freight on top of Level II. Line-item detail is transmitted to the network per item. |
How it comes together – you send vs the gateway adds
| You send (API fields) | The gateway adds (certified Cygma packet) |
|---|---|
sales_tax | Sales tax amount + collected indicator, merchant order reference, customer code, e-commerce indicator (Level II set) |
line_items[] | Per-item Level III TLV records (product code, description, commodity, quantity, unit of measure, amounts, debit/credit + discount indicators) – max 15 items |
freight | Freight amount + credit/debit indicator, plus discount and duty header fields with their indicators |
description | Merchant order / customer reference number |
cardholder.zip / merchant profile | Destination + ship-from postal codes, destination country, order date, entry coding |
| Field | Type | Description |
|---|---|---|
descriptionrequired | string ≤26 | What was sold. 26 characters is the card-network line-description limit. |
unit_costrequired | integer | Cents per unit. |
commodity_coderequired to qualify | string ≤15 | NIGP commodity code (numeric). The parser accepts its absence, but the card networks require it for the Level III rate – without it the line downgrades. Look up codes in the M360 inventory item editor, or download the full list (2,839 codes): Cygma docs → Level III. |
quantityrecommended | integer ≥1 | Whole units. Defaults to 1 when omitted. |
amountauto-derived | integer | Extended line total in cents. Defaults to quantity × unit_cost − discount – send it only when your own math differs (rounding). |
product_codeauto-derived | string ≤12 | SKU / product code. Derived from the description when omitted. |
unit_of_measureauto-derived | string ≤12 | Unit code – EA each (default), BX box, HR hour, LB pound… |
discountoptional | integer | Line discount cents (already reflected in the line total). Defaults to 0. |
Rules that matter
- Amounts must reconcile.
amount(the charge) = line totals +freight+sales_tax. The example: $89.99 + $5.95 + $7.76 = $103.70. - Max 15 line items – the processor limit. Longer orders: send the 15 largest lines; totals still reconcile via the header amounts.
- Send Level III only on full payments. A partial payment can’t reconcile to the line items – send
sales_tax(prorated) alone and it qualifies at Level II. - Consumer cards are unaffected. The extra data is ignored on non-commercial cards – always safe to send.
- Qualification is decided at settlement, not authorization. The approval response looks identical either way; the qualification level appears in the merchant’s interchange detail.
- Works with
surcharge: "auto"and on invoice payments (invoice line items carry Level III automatically when the invoice is paid in full).
curl -X POST https://www.merchant360.net/api/m360/v1/payments \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 14556,
"sales_tax": 1156,
"invoice_number": "INV-2001",
"customer_code": "ACME-0042",
"card": {
"number": "4242424242424242",
"exp_month": "03",
"exp_year": "2028",
"cvv": "123"
},
"cardholder": {
"name": "MICHAEL NARDY",
"zip": "11933",
"address": "1161 SCOTT AVE"
}
}'curl -X POST https://www.merchant360.net/api/m360/v1/payments \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 10370,
"sales_tax": 776,
"freight": 595,
"description": "INV-2002",
"card": {
"number": "4242424242424242",
"exp_month": "03",
"exp_year": "2028",
"cvv": "123"
},
"cardholder": {
"name": "MICHAEL NARDY",
"zip": "11933",
"address": "1161 SCOTT AVE"
},
"line_items": [
{
"description": "WHITENING GEL KIT 22PCT",
"commodity_code": "27055",
"quantity": 1,
"unit_cost": 8999
}
]
}'curl -X POST https://www.merchant360.net/api/m360/v1/payments \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 9624,
"sales_tax": 725,
"freight": 499,
"description": "INV-2005",
"card": {
"number": "4242424242424242",
"exp_month": "03",
"exp_year": "2028",
"cvv": "123"
},
"cardholder": {
"name": "MICHAEL NARDY",
"zip": "11933"
},
"line_items": [
{
"description": "STERILIZATION POUCHES BX",
"commodity_code": "46590",
"unit_of_measure": "BX",
"quantity": 2,
"unit_cost": 2450
},
{
"description": "HOME CARE KIT",
"commodity_code": "27055",
"quantity": 1,
"unit_cost": 4000,
"discount": 500,
"amount": 3500
}
]
}'Industry addenda
An industry addendum describes what was bought, in the vocabulary the card networks defined for that trade – gallons and a pump price for fuel, arrival and departure dates for a hotel, a ticket number and an itinerary for an airline. It is the same mechanism as Level III: the charge approves either way, and the data qualifies the transaction for the industry interchange rate.
Send it as an industry object on POST /v1/payments. typenames the industry; every other key is one of that industry’s fields. M360 validates the object, scales each value to the implied decimals its field uses, pads it to width, and adds the IndustryIndicatorthe processor needs. You send readable values – "4.2980", "2026-09-01"– and never a zero-filled integer.
Rules that matter
- Unknown field names are rejected. A typo returns a 400 naming the field rather than being dropped. Dropping it would authorize the sale, miss the industry rate, and leave you nothing to debug – the failure would show up weeks later in an interchange statement.
- Money and quantities are decimal STRINGS (
"3.4900"), unlikeamountwhich stays an integer number of cents. The scales differ per FIELD, and a single fuel object carries three of them – unit price to four places, quantity to three, the sale amount to two. M360 reads the scale off the field definition so you never have to; send the number as you would write it. - Dates are
YYYY-MM-DDand times areHH:MM. - An empty addendum is rejected. Naming an industry and sending no fields tells the processor to expect a hotel and then describes none. Send at least one field or omit
industryentirely. - Keyed charges only – with or without
surcharge: "auto". Acard_tokencharge has no packet hook for the addendum today. - Not every brand carries every industry. Where a profile or a field is defined by only some networks, the tables below say so, and M360 omits it on a card outside that set rather than sending data the network will not read. The clearest case is fuel:
"FUEL"is the Mastercard and Discover profile, and Visa fuel data rides"FLEET"– there is no separate Visa fuel profile. Send the one that matches the card. - Consumer cards are unaffected – the extra data is ignored, so it is always safe to send.
- Qualification is decided at settlement, not authorization. The approval looks identical either way.
Fuel and fleet product codes come from the Visa Fleet 2.0 tables, which are a different catalogue from the NIGP commodity codes used by Level III – and the fuel and non-fuel tables are separate namespaces from each other, so the same two characters mean different things in each. The full tables, with Conexxus equivalents: visa-fleet-product-codes.xlsx.
| Field | Type | Description |
|---|---|---|
"type": "FUEL"optional | J26 = 8 | Fuel — Pump and product detail for a fuel purchase. Mastercard and Discover carry different subsets; Visa fuel rides the Fleet profile. 14 fields. |
"type": "AIRLINE"optional | J26 = 1 | Airline — Ticket, passenger and itinerary detail. Captures the ticket header and the first air segment. 29 fields. |
"type": "HEALTHCARE"optional | J26 = 18 | Healthcare — Provider and payer identifiers. Per-line healthcare eligibility is set on the Level III grid. 3 fields. |
"type": "CRUISE"optional | J26 = 19 | Cruise — Sailing, itinerary and the air leg to the port, plus agency identifiers. 19 fields. |
"type": "RAIL"optional | J26 = 15 | Rail — Ticket, journey and service detail for rail travel. 19 fields. |
"type": "ELECTRIC_FUEL"optional | J26 = 26 | Electric vehicle charging — EV charging session — connector, energy, timings and station capacity. Required for MCC 5552 in Europe now and in the US by 2030. 15 fields. |
"type": "TRAVEL"optional | J26 = 17 | Travel agency — Agency identifiers and the service fee charged on a travel booking. 7 fields. |
"type": "INSURANCE"optional | J26 = 22 | Insurance — Policy, insured party and premium detail. 7 fields. |
"type": "TELEPHONE"optional | J26 = 14 | Telephone — Originating and destination numbers for a call-based charge. 3 fields. |
"type": "TICKET_ENTERTAINMENT"optional | J26 = 16 | Ticketing / entertainment — Event, venue and ticket detail. 9 fields. |
"type": "VISA_TRANSPORT_ANCILLARY"optional | J26 = 23 | Transport ancillary — Baggage, seating or other purchases attached to a travel document rather than the ticket. 4 fields. |
"type": "HOTEL"optional | J26 = 4 | Hotel / lodging — Lodging enhanced data - the stay, the room, and itemised incidentals. Carried on all brands. 27 fields. |
"type": "AUTO_RENTAL"optional | J26 = 6 | Auto rental — Vehicle rental enhanced data - agreement, pickup and return detail, distance and adjustments. 21 fields. |
"type": "FLEET"optional | J26 = 25 | Fleet — Visa Fleet enhanced data — fuel type, quantity, pricing and the driver/vehicle prompts a fleet card asks for. 17 fields. |
FuelJ26 = 814 fields · MC · DISC
Pump and product detail for a fuel purchase. Mastercard and Discover carry different subsets; Visa fuel rides the Fleet profile.
Consistency: fuel_quantity × fuel_unit_price must equal the amount authorized and fuel_sale_amount. Solve to the field’s own precision — the rounded product will not always reproduce the amount to the cent, and the amount is what is charged.
| Field | Type | Description |
|---|---|---|
company_brand_namerecommended | B62 · an4 | Brand — Brand at the pump, 4 characters (e.g. SHEL). Pre-filled from the merchant name. |
purchase_timerecommended | B63 · n4 · HHMM | Purchase time — Local time at the pump, HH:MM. MC only. |
fuel_service_typerecommended | B64 · an1 · enum | Service type Values: S, F, H. MC only. |
| Field | Type | Description |
|---|---|---|
fuel_coderequired to qualify | B69 · an2 · enum | Fuel code — Visa Fuel Type Code. 121 defined values. DISC only. |
fuel_unit_pricerequired to qualify | B71 · n12 · 4dp implied | Price per gallon — Dollars per gallon, e.g. 3.499. MC only. |
fuel_quantityrequired to qualify | B72 · n6 · 3dp implied | Quantity (gallons) — Gallons dispensed, e.g. 12.153. MC only. |
fuel_sale_amountrecommended | B73 · n12 · 2dp implied | Fuel sale amount — The fuel portion of the sale. MC only. |
| Field | Type | Description |
|---|---|---|
total_tax_amountrecommended | B65 · n12 · 2dp implied | Total tax |
total_tax_collect_indicatorrecommended | B66 · an1 · enum | Tax collected Values: Y, N. MC only. |
state_sales_tax_amountrecommended | B67 · n12 · 2dp implied | State sales tax — Pre-filled from the merchant's default tax rate in Settings. DISC only. |
state_sales_tax_idrequired to qualify | B68 · an1 | State tax ID — One character. DISC only. |
tax_exempt_numberrequired to qualify | B70 · n12 | Tax exempt number — Digits only, up to 12. DISC only. |
| Field | Type | Description |
|---|---|---|
odometer_readingrequired to qualify | H165 · n7 | Odometer — Whole miles. Digits only - no commas, no decimals. |
| Field | Type | Description |
|---|---|---|
exempt_indicatorrequired to qualify | — · an1 | Exempt indicator |
AirlineJ26 = 129 fields · All brands
Ticket, passenger and itinerary detail. Captures the ticket header and the first air segment.
| Field | Type | Description |
|---|---|---|
ticket_numberrequired to qualify | C02 · an15 | Ticket number |
passenger_namerequired to qualify | C10 · an25 | Passenger name |
transaction_typerequired to qualify | C01 · an2 | Transaction type |
document_typerequired to qualify | C03 · an2 | Document type |
ticket_issue_daterequired to qualify | C08 · n8 · YYYYMMDD | Issue date |
ticket_issue_cityrequired to qualify | C07 · an18 | Issue city |
ticketing_carrierrequired to qualify | C06 · an25 | Ticketing carrier |
iata_coderequired to qualify | C05 · n8 | IATA code |
electronic_ticketrecommended | C14 · an1 · enum | Electronic ticket Values: E, P. |
restricted_ticketrequired to qualify | C75 · an1 · enum | Restricted ticket Values: N, R. |
number_in_partyrecommended | C09 · n3 | Passengers |
total_farerecommended | C78 · n12 · 2dp implied | Total fare |
| Field | Type | Description |
|---|---|---|
total_segmentsrecommended | C15 · n2 | Air segments — Total legs on the ticket. Only the first is captured here. |
departure_locationrequired to qualify | C18 · an5 | From (airport) |
arrival_locationrequired to qualify | C20 · an5 | To (airport) |
departure_daterequired to qualify | C19 · n8 · YYYYMMDD | Departure date |
departure_timerequired to qualify | C79 · n4 · HHMM | Departure time — HH:MM |
arrival_timerequired to qualify | C80 · n4 · HHMM | Arrival time — HH:MM |
segment_carrierrequired to qualify | C21 · an4 | Carrier |
flight_numberrequired to qualify | C24 · an6 | Flight number |
class_of_servicerequired to qualify | C23 · an3 | Class of service |
fare_basisrequired to qualify | C22 · an15 | Fare basis |
segment_farerequired to qualify | C25 · n12 · 2dp implied | Segment fare |
stop_overrequired to qualify | C17 · an1 · enum | Stopover Values: O, X. |
| Field | Type | Description |
|---|---|---|
travel_agency_coderequired to qualify | C73 · an8 | Agency code |
travel_agency_namerequired to qualify | C74 · an25 | Agency name |
customer_coderequired to qualify | C71 · an17 | Customer code |
ticket_change_indicatorrequired to qualify | C77 · an1 | Ticket change |
credit_reason_indicatorrequired to qualify | C76 · an1 | Credit reason |
HealthcareJ26 = 183 fields · All brands
Provider and payer identifiers. Per-line healthcare eligibility is set on the Level III grid.
| Field | Type | Description |
|---|---|---|
provider_idrequired to qualify | P27 · an15 | Provider ID — Visa healthcare provider identifier. VISA only. |
service_type_coderequired to qualify | P28 · an4 | Service type VISA only. |
payer_idrequired to qualify | P31 · an15 | Payer ID |
CruiseJ26 = 1919 fields · All brands
Sailing, itinerary and the air leg to the port, plus agency identifiers.
| Field | Type | Description |
|---|---|---|
passenger_namerequired to qualify | C38 · an25 | Passenger name |
ticket_numberrequired to qualify | C39 · an15 | Ticket number |
cruise_namerequired to qualify | C50 · an25 | Cruise / ship name |
departure_daterequired to qualify | C46 · n8 · YYYYMMDD | Departure date |
return_daterequired to qualify | C47 · n8 · YYYYMMDD | Return date |
number_of_daysrequired to qualify | C49 · n3 | Nights |
total_costrecommended | C48 · n12 · 2dp implied | Total cost |
class_coderequired to qualify | C45 · an3 | Class |
travel_packagerequired to qualify | C37 · an1 · enum | Travel package Values: Y, N. |
| Field | Type | Description |
|---|---|---|
destination_coderequired to qualify | C41 · an5 | Destination |
city_namerequired to qualify | C53 · an18 | City |
region_coderequired to qualify | C51 · an3 | Region |
country_coderequired to qualify | C52 · an3 | Country |
| Field | Type | Description |
|---|---|---|
departure_airportrequired to qualify | C42 · an5 | Departure airport |
air_carrier_coderequired to qualify | C43 · an4 | Air carrier |
flight_numberrequired to qualify | C44 · an6 | Flight number |
depart_daterequired to qualify | C40 · n8 · YYYYMMDD | Flight date |
| Field | Type | Description |
|---|---|---|
iata_carrier_coderequired to qualify | C35 · an4 | IATA carrier |
iata_agency_numberrequired to qualify | C36 · an8 | IATA agency number |
RailJ26 = 1519 fields · All brands
Ticket, journey and service detail for rail travel.
| Field | Type | Description |
|---|---|---|
transaction_typerequired to qualify | C26 · an2 | Transaction type |
ticket_numberrequired to qualify | C27 · an15 | Ticket number |
passenger_namerequired to qualify | C28 · an25 | Passenger name |
carrier_coderequired to qualify | C29 · an4 | Carrier |
issuer_namerequired to qualify | C30 · an25 | Issuer name |
issuer_cityrequired to qualify | C31 · an18 | Issuer city |
| Field | Type | Description |
|---|---|---|
departure_locationrequired to qualify | C32 · an5 | From |
arrival_locationrequired to qualify | C34 · an5 | To |
departure_daterequired to qualify | C33 · n8 · YYYYMMDD | Departure date |
rail_classrequired to qualify | C60 · an3 | Class |
number_of_adultsrecommended | C58 · n3 | Adults |
number_of_childrenrequired to qualify | C59 · n3 | Children |
| Field | Type | Description |
|---|---|---|
traveller_namerequired to qualify | C54 · an25 | Traveller name |
service_ticket_numrequired to qualify | C55 · an15 | Service ticket number |
service_typerequired to qualify | C56 · an3 | Service type |
service_naturerequired to qualify | C57 · an3 | Service nature |
service_amountrequired to qualify | C61 · n12 · 2dp implied | Service amount |
service_amount_signrequired to qualify | C62 · an1 · enum | Amount sign Values: D, C. |
procedure_idrequired to qualify | C63 · an8 | Procedure ID |
Electric vehicle chargingJ26 = 2615 fields · All brands
EV charging session — connector, energy, timings and station capacity. Required for MCC 5552 in Europe now and in the US by 2030.
Consistency: quantity × unit_price must equal the amount authorized and total_including_tax. Solve to the field’s own precision — the rounded product will not always reproduce the amount to the cent, and the amount is what is charged.
| Field | Type | Description |
|---|---|---|
connector_typerequired to qualify | S30 · an3 · enum | Connector type 9 defined values. |
unit_of_measurerecommended | S37 · an1 · enum | Unit of measure — Electric sessions bill by kWh or by minute. Values: W, C. |
quantityrequired to qualify | S42 · n12 · 4dp implied | Quantity (kWh) |
unit_pricerequired to qualify | S39 · n12 · 4dp implied | Price per kWh |
total_including_taxrecommended | S48 · n12 · 2dp implied | Total including tax |
start_timerequired to qualify | S46 · n4 · HHMM | Charge start — HH:MM |
finish_timerequired to qualify | S47 · n4 · HHMM | Charge finish — HH:MM |
total_charging_timerequired to qualify | S45 · n6 | Charging time (min) |
total_time_plugged_inrequired to qualify | S44 · n6 | Plugged in (min) — Can exceed charging time - idle minutes are often billed separately. |
| Field | Type | Description |
|---|---|---|
max_power_dispensedrequired to qualify | S31 · n6 | Max power dispensed (kW) |
power_capacityrequired to qualify | S36 · n6 | Station capacity (kW) — May exceed max dispensed when the site manages power. |
charging_reason_coderequired to qualify | S35 · an3 · enum | Charging reason — Only when the session ended abnormally. 10 defined values. |
| Field | Type | Description |
|---|---|---|
est_miles_addedrequired to qualify | S34 · n6 | Est. miles added |
est_vehicle_miles_availablerequired to qualify | S32 · n6 | Est. range on leaving |
carbon_footprintrequired to qualify | S33 · n12 | Carbon avoided (g CO2e) |
Travel agencyJ26 = 177 fields · All brands
Agency identifiers and the service fee charged on a travel booking.
| Field | Type | Description |
|---|---|---|
agency_coderequired to qualify | H101 · an8 | Agency code |
agency_namerecommended | H102 · an25 | Agency name |
agency_seq_numberrequired to qualify | H085 · an8 | Sequence number |
| Field | Type | Description |
|---|---|---|
fee_amountrequired to qualify | H086 · n12 · 2dp implied | Agency fee |
fee_amount_signrecommended | H087 · an1 · enum | Fee sign Values: D, C. |
fee_raterequired to qualify | H088 · n6 · 2dp implied | Fee rate (%) |
fee_descriptionrecommended | H089 · an25 | Fee description |
InsuranceJ26 = 227 fields · All brands
Policy, insured party and premium detail.
| Field | Type | Description |
|---|---|---|
policy_numberrequired to qualify | H148 · an25 | Policy number |
additional_policy_numberrequired to qualify | H152 · an25 | Additional policy number |
type_of_policyrequired to qualify | H153 · an25 | Policy type |
name_of_insuredrequired to qualify | H154 · an30 | Name of insured |
| Field | Type | Description |
|---|---|---|
premium_frequencyrequired to qualify | H151 · an12 · enum | Premium frequency Values: Monthly, Quarterly, Annual, Single. |
insurance_amountrecommended | H077 · n12 · 2dp implied | Premium amount |
insurance_indicatorrecommended | H131 · an1 · enum | Insurance indicator Values: Y, N. |
TelephoneJ26 = 143 fields · All brands
Originating and destination numbers for a call-based charge.
| Field | Type | Description |
|---|---|---|
call_from_phone_numberrequired to qualify | J73 · n15 · digits only | Call from |
call_to_phone_numberrequired to qualify | J77 · n15 · digits only | Call to |
phone_card_idrequired to qualify | J78 · an20 | Phone card ID |
Ticketing / entertainmentJ26 = 169 fields · All brands
Event, venue and ticket detail.
| Field | Type | Description |
|---|---|---|
event_namerecommended | J60 · an25 | Event name |
event_daterequired to qualify | J61 · n8 · YYYYMMDD | Event date |
event_locationrequired to qualify | J64 · an25 | Venue |
event_region_coderequired to qualify | J65 · an3 | Region |
event_country_coderequired to qualify | J66 · an3 | Country |
| Field | Type | Description |
|---|---|---|
ticket_quantityrecommended | J63 · n4 | Tickets |
individual_ticket_pricerequired to qualify | J62 · n12 · 2dp implied | Price per ticket |
ticket_typerequired to qualify | C86 · an4 | Ticket type |
issuer_addressrequired to qualify | C83 · an25 | Issuer address |
Transport ancillaryJ26 = 234 fields · All brands
Baggage, seating or other purchases attached to a travel document rather than the ticket.
| Field | Type | Description |
|---|---|---|
ticket_document_numberrequired to qualify | C65 · an15 | Ticket document number |
additional_document_numberrequired to qualify | C69 · an15 | Additional document number |
passenger_namerequired to qualify | C68 · an25 | Passenger name |
credit_reason_indicatorrequired to qualify | C70 · an1 | Credit reason |
Hotel / lodgingJ26 = 427 fields · All brands
Lodging enhanced data - the stay, the room, and itemised incidentals. Carried on all brands.
| Field | Type | Description |
|---|---|---|
arrival_daterequired to qualify | H016 · n8 · YYYYMMDD | Arrival date |
departure_daterequired to qualify | H017 · n8 · YYYYMMDD | Departure date |
folio_numberrequired to qualify | H018 · an12 | Folio number |
room_raterequired to qualify | H021 · n12 · 2dp implied | Room rate (nightly) |
room_taxrequired to qualify | H022 · n12 · 2dp implied | Room tax |
number_of_roomsrecommended | H008 · n3 | Rooms booked |
number_of_adultsrequired to qualify | H009 · n3 | Adults |
no_show_indicatorrecommended | H011 · an1 · enum | No-show Values: N, Y. |
| Field | Type | Description |
|---|---|---|
room_typerequired to qualify | H006 · an12 | Room type |
bed_typerequired to qualify | H005 · an12 | Bed type |
room_locationrequired to qualify | H004 · an12 | Room location |
smoking_preferencerequired to qualify | H007 · an1 · enum | Smoking Values: N, S. |
rate_typerequired to qualify | H012 · an12 | Rate type |
program_coderequired to qualify | H023 · an12 | Program code |
promotional_coderequired to qualify | H001 · an12 | Promotional code |
corporate_client_coderequired to qualify | H003 · an12 | Corporate client code |
| Field | Type | Description |
|---|---|---|
phone_chargesrequired to qualify | H024 · n12 · 2dp implied | Phone |
restaurant_chargesrequired to qualify | H025 · n12 · 2dp implied | Restaurant / room service |
mini_bar_chargesrequired to qualify | H026 · n12 · 2dp implied | Mini bar |
laundry_chargesrequired to qualify | H027 · n12 · 2dp implied | Laundry |
gift_shop_chargesrequired to qualify | H030 · n12 · 2dp implied | Gift shop |
movie_chargesrequired to qualify | H032 · n12 · 2dp implied | Movies |
health_club_chargesrequired to qualify | H033 · n12 · 2dp implied | Health club |
valet_parking_chargesrequired to qualify | H034 · n12 · 2dp implied | Valet parking |
cash_disbursement_chargesrequired to qualify | H035 · n12 · 2dp implied | Cash disbursement |
other_chargesrequired to qualify | H028 · n12 · 2dp implied | Other |
adjustment_amountrequired to qualify | H020 · n12 · 2dp implied | Adjustment |
Auto rentalJ26 = 621 fields · All brands
Vehicle rental enhanced data - agreement, pickup and return detail, distance and adjustments.
| Field | Type | Description |
|---|---|---|
agreement_numberrequired to qualify | B01 · an25 | Agreement number — Rental agreement number signed by the cardholder. |
rate_indicatorrequired to qualify | B38 · an1 · enum | Rate type Values: D, W, M. |
raterequired to qualify | B39 · n12 · 2dp implied | Rate |
vehicle_class_idrequired to qualify | B14 · an4 | Vehicle class |
driver_tax_numberrequired to qualify | B22 · an20 | Driver tax number |
| Field | Type | Description |
|---|---|---|
pickup_daterequired to qualify | B06 · n8 · YYYYMMDD | Pickup date |
pickup_timerequired to qualify | B07 · n4 · HHMM | Pickup time — HH:MM |
pickup_locationrequired to qualify | B02 · an26 | Location |
pickup_cityrequired to qualify | B03 · an18 | City |
pickup_regionrequired to qualify | B04 · an3 | State / region |
pickup_countryrequired to qualify | B05 · an3 | Country |
| Field | Type | Description |
|---|---|---|
return_daterequired to qualify | B11 · n8 · YYYYMMDD | Return date |
return_timerequired to qualify | B12 · n4 · HHMM | Return time — HH:MM |
dropoff_locationrequired to qualify | B19 · an26 | Drop-off location |
return_cityrequired to qualify | B08 · an25 | City |
return_regionrequired to qualify | B09 · an3 | State / region |
return_countryrequired to qualify | B10 · an3 | Country |
distancerequired to qualify | B15 · n5 | Distance travelled — Whole units. |
distance_uomrecommended | B16 · an1 · enum | Distance unit Values: M, K. |
adjustment_indicatorrequired to qualify | B17 · an1 | Adjustment type |
adjustment_amountrequired to qualify | B18 · n12 · 2dp implied | Adjustment amount |
FleetJ26 = 2517 fields · VISA
Visa Fleet enhanced data — fuel type, quantity, pricing and the driver/vehicle prompts a fleet card asks for.
Consistency: quantity × per_unit_cost must equal the amount authorized and gross_fuel_price. Solve to the field’s own precision — the rounded product will not always reproduce the amount to the cent, and the amount is what is charged.
| Field | Type | Description |
|---|---|---|
business_application_idrecommended | P25 · an2 | Business application — Fleet business application identifier. F1 per Cygma's Visa Fleet sample. |
| Field | Type | Description |
|---|---|---|
fleet_fuel_typeoptional | H157 · an2 | Fuel type (2-char) — Two-character fuel type, e.g. GA. Sent alongside the expanded fuel type. Applies only when type_of_purchase is “1” or “3” or “4”. Fuel only. |
| Field | Type | Description |
|---|---|---|
type_of_purchaserequired to qualify | S51 · an1 · enum | Type of purchase — MANDATORY on fleet. Drives which of the fields below Visa requires. Values: 1, 2, 3, 4. |
expanded_fuel_typeoptional | S25 · an4 · enum | Fuel type — Visa Fuel Type Code. Required when type of purchase is 1, 3 or 4; blank when 2. 121 defined values. Applies only when type_of_purchase is “1” or “3” or “4”. Fuel only - not sent on a non-fuel purchase. |
service_typeoptional | S52 · an1 · enum | Service type Values: S, F, H. Applies only when type_of_purchase is “1” or “3” or “4”. Fuel only - not sent on a non-fuel purchase. |
unit_of_measureoptional | S37 · an1 · enum | Unit of measure 7 defined values. Applies only when type_of_purchase is “1” or “3” or “4”. Fuel only - not sent on a non-fuel purchase. |
quantityoptional | S42 · n12 · 4dp implied | Quantity (gallons) — Gallons dispensed, e.g. 12.153. Four implied decimals. Applies only when type_of_purchase is “1” or “3” or “4”. Fuel only - Visa requires 0 here on a non-fuel purchase. |
per_unit_costoptional | S39 · n12 · 4dp implied | Price per gallon — Dollars per gallon, e.g. 3.499. Four implied decimals. Applies only when type_of_purchase is “1” or “3” or “4”. Fuel only - Visa requires 0 here on a non-fuel purchase. |
gross_fuel_priceoptional | S55 · n12 · 4dp implied | Gross fuel price — Must equal quantity x unit cost, INCLUSIVE of taxes. Applies only when type_of_purchase is “1” or “3” or “4”. Fuel only - Visa requires 0 here on a non-fuel purchase. |
net_fuel_priceoptional | H162 · n12 · 4dp implied | Net fuel price — Optional. Quantity x cost EXCLUSIVE of taxes. Applies only when type_of_purchase is “1” or “3” or “4”. Fuel only. |
| Field | Type | Description |
|---|---|---|
gross_non_fuel_priceoptional | H163 · n12 · 2dp implied | Gross non-fuel price — Required when type of purchase is 2 or 3. Sum of the line items, inclusive of taxes. Applies only when type_of_purchase is “2” or “3”. Non-fuel only - Visa requires 0 here on a fuel-only purchase. |
net_non_fuel_priceoptional | H164 · n12 · 2dp implied | Net non-fuel price — Optional, exclusive of taxes. Applies only when type_of_purchase is “2” or “3”. Non-fuel only. |
| Field | Type | Description |
|---|---|---|
odometer_readingrequired to qualify | H165 · n7 | Odometer — Whole miles. Digits only - no commas, no decimals. |
employee_numberrecommended | S26 · an12 | Employee number — When the card prompts for it. Defaults to 1. |
trailer_numberrequired to qualify | S27 · an16 | Trailer number — When the card prompts for it. |
prompted_data1required to qualify | S28 · an20 | Prompted data 1 |
prompted_data2required to qualify | S29 · an20 | Prompted data 2 |
The industry dictionary also enumerates Visa Limited Data, Mail order / telephone order, Retail, Temporary services. Those are classifications rather than data sets – they carry no fields, so there is nothing to send.
// POST /v1/payments — a fuel sale on a MASTERCARD.
//
// Prices and quantities are DECIMAL STRINGS here, unlike
// the cents integer used for amount. M360 scales each one
// to the implied decimals its own field uses — and those
// differ within this one object (4, 3 and 2 places).
{
"amount": 1500,
"card": { "number": "5454545454545454", "exp_month": "12", "exp_year": "2028" },
"industry": {
"type": "FUEL",
"company_brand_name": "SHEL",
"purchase_time": "14:32",
"fuel_service_type": "S",
"fuel_unit_price": "3.4900",
"fuel_quantity": "4.298",
"fuel_sale_amount": "15.00",
"total_tax_amount": "1.17",
"total_tax_collect_indicator": "Y",
"odometer_reading": "84231"
}
}Authorize & capture
/payments/authorizePlace a hold (no money moves)./payments/{id}/incrementRaise the hold – incremental auth (bar tab / hospitality): { "amount": 500, "card": {…} }./payments/{id}/captureSettle up to the authorized amount (partial ok): { "amount": 4200 }./payments/{id}/voidRelease the hold (no body)./payments/{id}/tip-adjustAdd a written tip to an unsettled sale: { "tip_amount": 500 }.Authorize places a hold and returns status: "requires_capture" with amount_capturable – no funds move until you capture. Capture the full amount or less; capturing less settles that amount and releases the remainder to the cardholder automatically. A void releases the entire hold. If you never do either, the issuer expires the hold on its own (typically ~7 days) – nothing to clean up on your side, but the funds stay unavailable to the cardholder until then.
| Field | Type | Description |
|---|---|---|
requires_captureoptional | status | Hold placed; amount_capturable shows what you can still capture. |
succeededoptional | status | Captured (full or partial) – the captured amount joins the next settlement batch. |
Every transition has a webhook: payment.authorized when the hold lands, payment.captured on capture, payment.voided on release.
| Field | Type | Description |
|---|---|---|
amountrequired | integer | Amount to authorize, in cents (> 0). |
currencyoptional | string | Only usd (default). |
card.numberrequired | string | PAN (keyed). Saved-card pre-auth is a planned follow-up. |
card.exp_monthrequired | string | Two digits, 01–12. |
card.exp_yearrequired | string | Two or four digits. |
card.cvvoptional | string | Security code. |
cardholder.*optional | object | Same shape as POST /payments (name/email/phone/zip/address, for AVS). |
descriptionoptional | string | Reference on the transaction (≤120 chars). |
| Field | Type | Description |
|---|---|---|
amountoptional | integer | Cents to capture. Omit for the full authorized amount; cannot exceed it. |
Incremental authorization (bar tabs, hospitality): raise an open hold before capturing. {id} is the original authorization. amount is the ADDITIONAL cents; the gateway tracks and sends the cumulative total per the Cygma incremental spec. The card must be presented again (card keyed or card_token saved) – holds don’t retain it. Example flow: authorize $10.00 → increment $5.00 (tab $15.00) → increment $10.00 (tab $25.00) → capture $25.00 → tip-adjust to $30.00. Requires the authorization class enabled on the merchant’s terminal profile – contact EPI to enable it.
| Field | Type | Description |
|---|---|---|
amountrequired | integer | ADDITIONAL cents to authorize (> 0). |
card / card_tokenrequired | object / string | The card again – keyed, or a saved card on file. |
cumulative_amountoptional | integer | Override the computed running total (original + approved increments + this amount). |
descriptionoptional | string | Reference on the increment transaction. |
Tip adjust (restaurant receipt flow): after capturing a sale, add the written tip while the sale is still in the open batch. The tip is itemized to the network (DE54 type 43). After batch close the amount is final – a late tip needs a separate charge.
| Field | Type | Description |
|---|---|---|
tip_amountrequired | integer | Tip cents being added (> 0). |
amountoptional | integer | Corrected TOTAL cents including the tip; defaults to the sale amount + tip_amount. |
Void vs refund: void releases a hold or reverses a same-batch charge before settlement. Once a charge has settled, use a refund – a void on a settled transaction returns 409.
curl -X POST https://www.merchant360.net/api/m360/v1/payments/authorize \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 1000,
"card": {
"number": "4242424242424242",
"exp_month": "12",
"exp_year": "2027",
"cvv": "123"
},
"description": "TAB 14"
}'curl -X POST https://www.merchant360.net/api/m360/v1/payments/AUTH_ID/increment \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 500,
"card": {
"number": "4242424242424242",
"exp_month": "12",
"exp_year": "2027",
"cvv": "123"
}
}'curl -X POST https://www.merchant360.net/api/m360/v1/payments/AUTH_ID/capture \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 2500
}'curl -X POST https://www.merchant360.net/api/m360/v1/payments/TXN_ID/tip-adjust \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"tip_amount": 500
}'curl -X POST https://www.merchant360.net/api/m360/v1/payments/AUTH_ID/void \ -H "Authorization: Bearer m360_live_xxx"
Refunds & voids
/payments/{id}/refundRefund a settled charge (full or partial; omit amount for full)./payments/{id}/voidVoid a hold or unsettled charge (no body; 409 if already settled).The dividing line is settlement. A void works on an authorization or an unsettled sale – it cancels the transaction before it batches, so nothing ever appears on the cardholder’s statement. Once the sale has settled, a void returns 409 already_settled; use a refund instead, which moves money back to the cardholder as its own transaction.
Use a void before settlement (releases a hold / reverses a same-batch sale) and a refund after. A pre-settlement refund is itself processed as a reversal.
A refund is its own object (object: "refund", status: "succeeded") – full, or partial by passing an amount smaller than the charge; you can refund the remainder later. Refunds are gated by the merchant’s channel profile: when the program has refunds disabled, the API returns 403 not_permitted_by_channel even with a valid key and scope. Both moves have webhooks – payment.refunded and payment.voided – so downstream systems hear about reversals they didn’t initiate.
curl -X POST https://www.merchant360.net/api/m360/v1/payments/{id}/refund \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 2500
}'Cards & tokenization (vault)
Tokenization turns a card into a reusable card token – a handle for a card on file. Tokenizing needs no amount and no charge: POST /tokens vaults any card outright (the token can be charged or removed later). You can also mint a token as a side effect of a sale by setting save_card on a payment. Charge a saved token by passing card_token to POST /payments, or reference one from a subscription. The raw PAN is never returned – only brand + last4 + expiry.
Vaulting a card fires a card.created webhook whichever way you mint it. Deleting a token is a soft revoke – the card stops being chargeable, but history that references it stays intact – and it’s idempotent: deleting an already-deleted token succeeds.
| Field | Type | Description |
|---|---|---|
card.numberrequired | string | PAN to vault, digits (spaces ok). |
card.exp_monthrequired | string | Two digits, 01–12. |
card.exp_yearrequired | string | Two or four digits (27 or 2027). |
cardholder.nameoptional | string | Label for the saved card. |
cardholder.zipoptional | string | Billing ZIP stored for AVS on future charges. |
cardholder.addressoptional | string | Billing street stored for AVS. |
set_defaultoptional | boolean | Make this the merchant's default card on file. |
▶ Try the live vault demo – save a card, then charge it repeatedly.
/tokensTokenize a card without charging it (cards:write)./tokensList saved cards on file (cards:read). ?search= filters by cardholder or the linked customer's name/email; each card carries a customer block when attached to one – powers “charge a card on file” pickers./tokens/{id}Retrieve one saved card (cards:read)./tokens/{id}Remove a saved card (cards:write).Tokenizing a card without a charge (POST /tokens) is temporarily unavailable in live mode – it returns 503 tokenization_unavailable until the tokenization service is certified. Test mode returns a simulated token, so you can build the full flow today; saving a card via save_card on a live charge also works today.
curl -X POST https://www.merchant360.net/api/m360/v1/tokens \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"card": {
"number": "4242424242424242",
"exp_month": "12",
"exp_year": "2027"
},
"cardholder": {
"name": "Jane Doe",
"zip": "90210"
},
"set_default": true
}'Hosted fields
Hosted fields render the card inputs in a Merchant360-hosted iframe embedded on your page; the card is entered on the Merchant360 origin. The browser exchanges the card for a reusable card_token; you then charge that token server-side with your secret key. Your page handles the token only (SAQ A eligible).
Try the live demo →Runs the real embed in test mode – no key needed.
Two calls make it work:
/hosted/sessionsYour server mints a short-lived client_token (cards:write). Never expose your secret key to the browser – this token is what the browser uses./hosted/tokenizeCalled by the iframe (not you) to turn the card into a card_token.| Field | Type | Description |
|---|---|---|
expires_inoptional | integer | Client-token lifetime in seconds. Default 900 (15 min); clamped to 60–3600. |
| Field | Type | Description |
|---|---|---|
Merchant360(clientToken)optional | fn | Create an instance from the client token minted in step 1. |
.hostedFields({ style })optional | fn | Create the fields. style.accent + style.fontFamily theme them. |
.mount(selector)optional | fn | Insert the iframe into your page. |
.on('change', cb)optional | fn | Live field validity → { complete, brand }. |
.tokenize()optional | fn | Promise → { card_token, last4, brand }, or rejects with an error. |
The client token authorizes tokenization only; it cannot charge. It lives 15 minutes by default (expires_in clamps to 60–3600 seconds); if the customer outlasts the session, tokenize() rejects with an expiry error and you mint a new session and re-initialize. Live tokenization is temporarily unavailable and returns 503 until the tokenization service is certified; use a test-modekey (the session inherits the key’s mode and returns a simulated token) to build the full browser flow today. 3-D Secure is not part of this flow (it requires a separate EMV 3DS certification).
// 1) On YOUR SERVER, mint a short-lived client token with your secret key:
// POST https://www.merchant360.net/api/m360/v1/hosted/sessions → { "client_token": "hfs_…", "expires_at": "…" }
// 2) On YOUR PAGE, drop in the fields:
<script src="https://www.merchant360.net/merchant360.js"></script>
<div id="card"></div>
<button id="save" disabled>Save card</button>
<script>
var m360 = Merchant360(CLIENT_TOKEN); // from step 1
var fields = m360.hostedFields({ style: { accent: "#2563eb" } });
fields.mount("#card");
fields.on("change", function (e) { // { complete, brand }
document.getElementById("save").disabled = !e.complete;
});
document.getElementById("save").addEventListener("click", function () {
fields.tokenize()
.then(function (r) { // { card_token, last4, brand }
// POST r.card_token to YOUR server; charge it with your secret key:
// POST https://www.merchant360.net/api/m360/v1/payments { "amount": 2500, "card_token": r.card_token }
})
.catch(function (err) { alert(err.message); });
});
</script>ACH debits & credits
/ach/debitsPull from a customer bank account./ach/creditsPush to a customer bank account (same body)./ach/{id}Retrieve an entry – poll its status, or use the webhooks below./ach/routing/{number}Routing-number lookup: Mod-10 checksum + FedACH bank name – show “Debiting <bank>” before you submit. Any valid key; no scope./ach/authorization-requestsDon't hold the customer's authorization? Email them a secure page instead – they add their own bank, sign the Nacha consent, and the one-time debit runs automatically on approval. Body: { amount, customer: { name, email }, description? }. The signed authorization is stored on file for you.ACH is a delayed-notification payment method: creating a debit or credit tells you the entry was accepted, not that money moved. A successful create returns 201 with status: "queued" and an id (ach_…) – hold on to it. Entries batch to the bank at 4:00 PM ET each banking day; standard entries settle the next banking day. Same-day entries must be queued before noon ET – after that they automatically downgrade to standard. Once submitted, the entry is processing; settlement typically lands in 1–2 banking days, moving it to settled. If the bank rejects it, the entry becomes returned with a return: { code, description } explaining why.
| Field | Type | Description |
|---|---|---|
queuedoptional | status | Accepted, waiting for the next 4:00 PM ET submission window. No money has moved. |
processingoptional | status | Submitted to the bank; awaiting settlement (typically 1–2 banking days). |
settledoptional | status | Funds moved; settled_at is set. A late bank return can still move a settled entry to returned – see below. |
returnedoptional | status | The bank returned the entry; return.code / return.description say why. |
canceledoptional | status | Canceled before submission. Terminal. |
failedoptional | status | Rejected during submission. Terminal. |
How do you know it worked? You can poll GET /ach/{id}, but we recommend webhooks: subscribe to ach.queued, ach.settled, and ach.returned and you hear about every transition without polling – each payload is the same ach_payment object the GET returns. Treat ach.settled as your “funds are good” signal, not the 201.
A 202 with status: "held" means the entry is under risk review before queueing – id is null at this point. After review it either queues normally or is declined; no action is needed from you.
Returns can arrive well after settlement – a consumer unauthorized return may come up to 60 days after the statement date, so don’t treat settled as unconditionally final on debits. Insufficient-funds returns (R01/R09) are automatically re-presented up to two more times, about 2 banking days apart; the original entry’s resubmitted_as links to the new entry. All other return codes are final.
sec_code is resolved server-side from the account holder type (business → CCD, consumer → PPD/WEB) and returned on the entry – you never set it. Credits are subject to the merchant’s credit policy; both directions obey per-transaction / monthly volume limits. ▶ Try the live demo.
| Field | Type | Description |
|---|---|---|
idoptional | string | Entry id (ach_…). |
objectoptional | string | ach_payment. |
kindoptional | string | debit or credit. |
statusoptional | string | queued · processing · settled · returned · canceled · failed. |
amount_centsoptional | integer | Amount in cents. |
currencyoptional | string | usd. |
sec_codeoptional | string | PPD / WEB / CCD – resolved server-side (above). |
run_dateoptional | string | Banking day the entry submits (or submitted) to the bank. |
createdoptional | string | ISO timestamp. |
settled_atoptional | string | ISO timestamp once settled; null before. |
returnoptional | object | { code, description } once returned; null otherwise. |
resubmitted_asoptional | string | Id of the automatic re-presentment when an R01/R09 was retried. |
| Field | Type | Description |
|---|---|---|
400 no_authoptional | error | Debits require a stored customer authorization. |
403 not_enabled / not_allowedoptional | error | ACH is an EPI-approved capability, not on by default – the merchant isn't enabled for ACH, or not for this direction (debit vs credit). |
422 over_limitoptional | error | Per-transaction or monthly volume cap exceeded. |
422 not_validatedoptional | error | The bank account hasn't completed verification – a prenote takes 1–2 banking days. |
502 queue_failedoptional | error | The entry could not be queued. |
Charging a saved bank? Every successful create returns a bank_account_id (a bank_… token) for the account it used. Pass that back as bank_account_id on a later debit/credit to charge the same account again without re-sending the routing/account – the stored account is used server-side and never re-transmitted. When you pass bank_account_id, the bank_account block is not required.
| Field | Type | Description |
|---|---|---|
amountrequired | integer | Cents (> 0). |
bank_account_idoptional | string | Charge a saved bank on file (a bank_… token from a prior response). Use INSTEAD of bank_account; when present the keyed fields below aren’t needed. |
bank_account.routingoptional | string | 9-digit ABA routing number. Required unless bank_account_id is given. |
bank_account.accountoptional | string | Account number. Required unless bank_account_id is given. |
bank_account.holder_nameoptional | string | Name on the account. Required unless bank_account_id is given. |
bank_account.typeoptional | string | checking (default) or savings. |
bank_account.holder_typeoptional | string | consumer (PPD/WEB, default) or business (CCD). |
authorization.frequencyoptional | string | one_time (default) or recurring. |
descriptionoptional | string | NACHA entry description (≤10 chars). |
The create response also includes bank_account_id (the reusable bank_… token for the account charged). A 404 not_found means the bank_account_id isn’t a saved bank on this merchant.
Receipts. A bank debit settles a couple of business days out, so any receipt is emailed on settlement, not at submit. When you charge a saved bank by bank_account_id and the entry isn’t tied to an invoice or payment link, the receipt goes to the receipt_email stored on that saved bank (set it when you create the bank token). If no receipt email is on file, no receipt is sent. Invoice/payment-link debits keep receipting the customer on that document.
curl -X POST https://www.merchant360.net/api/m360/v1/ach/debits \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"bank_account": {
"routing": "021000021",
"account": "123456789",
"type": "checking",
"holder_name": "Jane Doe",
"holder_type": "consumer"
},
"authorization": {
"frequency": "one_time"
},
"description": "INVOICE42"
}'Saved banks (vault)
A bank token is a reusable handle for a customer bank account on file – the ACH analogue of a card token. Storing a bank moves no money (a $0 prenote validation fires). Charge a saved bank by passing its bank_account_id to POST /ach/debits or /ach/credits, so you never re-send the routing/account. The full account number is encrypted and never returned – only the routing (public ABA) and last 4.
/banksStore a bank on file without charging (banks:write). Returns the bank_… token./banksList saved banks (banks:read)./banks/{id}Retrieve one saved bank (banks:read)./banks/{id}Remove a saved bank – a soft archive (banks:write).| Field | Type | Description |
|---|---|---|
bank_account.routingrequired | string | 9-digit ABA routing number. |
bank_account.accountrequired | string | Account number (stored encrypted; only last 4 is returned). |
bank_account.holder_namerequired | string | Name on the account. |
bank_account.typeoptional | string | checking (default) or savings. |
bank_account.holder_typeoptional | string | consumer (PPD/WEB, default) or business (CCD). |
receipt_emailoptional | string | Optional. Where the receipt is emailed when this bank is charged standalone (a debit not tied to an invoice or payment link). Sent on settlement. |
| Field | Type | Description |
|---|---|---|
idoptional | string | The bank_… token – pass it as bank_account_id on an ACH debit/credit. |
objectoptional | string | Always bank_account. |
holder_name / routing / last4optional | string | Account holder, public ABA routing, and last 4 (never the full account). |
account_type / holder_typeoptional | string | checking/savings and consumer/business. |
sec_codeoptional | string | PPD / CCD derived from holder_type (informational). |
validation_statusoptional | string | unverified → prenote_pending → prenote_verified / verified (or failed). A prenote takes 1–2 banking days. |
is_defaultoptional | boolean | The merchant's default bank for this customer scope. |
receipt_emailoptional | string | The email a standalone-charge receipt is sent to (null when none is on file), echoed back from the create body. |
Storing a bank needs the account’s ACH capability to be enabled (like the ACH endpoints) – otherwise POST /banks answers 403 not_enabled. Test-mode keys return a simulated bank_test_…. A charge against a saved bank still obeys the debit-authorization and volume rules of the ACH endpoints.
curl -X POST https://www.merchant360.net/api/m360/v1/banks \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"bank_account": {
"routing": "021000021",
"account": "123456789",
"type": "checking",
"holder_name": "Jane Doe",
"holder_type": "consumer"
}
}'Customers
/customersCreate a customer./customersList customers (?search=)./customers/{id}Retrieve a customer./customers/{id}Update a customer./customers/{id}/autopayAutopay status (card on file that auto-pays future invoices)./customers/{id}/autopayEnroll a saved card (card_token + consent:true) so future invoices auto-charge on their due date./customers/{id}/autopayTurn autopay off.Customers have no lifecycle to manage – there is no status beyond active, no webhook to wait on, and creates/updates take effect immediately. Archiving a customer hides them from lists; everything referencing them (payments, invoices, subscriptions) stays intact.
| Field | Type | Description |
|---|---|---|
namerequired | string | Display name (required on create). |
legal_nameoptional | string | Legal/business name. |
emailoptional | string | Email. |
phoneoptional | string | Phone. |
address.line1optional | string | Billing street. |
address.city / state / zipoptional | string | Billing city / state / ZIP. |
curl -X POST https://www.merchant360.net/api/m360/v1/customers \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Jane Doe",
"email": "jane@example.com",
"phone": "555-0100",
"address": {
"line1": "1 Main St",
"city": "Austin",
"state": "TX",
"zip": "78701"
}
}'Catalog & inventory
/catalogCreate a catalog item./catalogList items (?include_pos=true to include register-only menu items)./catalog/{id}Retrieve an item./catalog/{id}Update an item./catalog/{id}Archive an item./catalog/{id}/stockRecord a stock movement.| Field | Type | Description |
|---|---|---|
namerequired | string | Product/service name. |
pricerequired | integer | Unit price in cents. |
costoptional | integer | Unit cost in cents. |
skuoptional | string | Stock-keeping unit. |
categoryoptional | string | Category label. |
descriptionoptional | string | Description. |
typeoptional | string | product (default), service, or digital. |
taxableoptional | boolean | Whether tax applies. Default true. |
track_stockoptional | boolean | Track on-hand quantity. Default false. |
quantityoptional | integer | Opening on-hand (when tracking stock). |
pos_itemoptional | boolean | Show in the TableTurn register. Default false (catalog-only). |
| Field | Type | Description |
|---|---|---|
quantityrequired | integer | Non-zero; positive receives, negative removes. |
reasonoptional | string | received, returned, adjustment (default), or sold. |
notesoptional | string | e.g. a PO number. |
curl -X POST https://www.merchant360.net/api/m360/v1/catalog \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Widget",
"sku": "W-1",
"price": 1999,
"cost": 800,
"taxable": true,
"track_stock": true,
"quantity": 100
}'Tax rates
/tax-ratesList tax rates./tax-ratesCreate a tax rate./tax-rates/{id}Update a tax rate./tax-rates/{id}Delete a tax rate.Apply a rate to an invoice by passing tax_rate_bps on the invoice (basis points; 625 = 6.25%).
| Field | Type | Description |
|---|---|---|
namerequired | string | Label (≤40 chars). |
rate_bpsrequired | integer | Rate in basis points (625 = 6.25%), 0–5000. |
stateoptional | string | Two-letter state code this rate applies to. |
defaultoptional | boolean | Make this the default rate. |
curl -X POST https://www.merchant360.net/api/m360/v1/tax-rates \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "TX state",
"state": "TX",
"rate_bps": 625,
"default": true
}'Invoices
/invoicesCreate an invoice (add "send": true to email it)./invoicesList invoices (?status=)./invoices/{id}Retrieve an invoice./invoices/{id}/payCharge a card (card_token or keyed card) against the invoice; flips it to partial/paid./invoices/{id}/sendEmail the invoice to the customer./invoices/{id}/textText (SMS) the pay link to the customer – STOP-compliant./invoices/{id}/voidVoid the invoice.The response includes the customer’s hosted_url (the pay page). ▶ Try the live demo.
An invoice moves DRAFT → OPEN → PARTIAL → PAID; VOID is terminal. Sending it (send: true on create, or POST /invoices/{id}/send) makes it OPEN; from there the customer drives it by paying on the hosted page. Card payments advance the invoice instantly. Bank/eCheck payments advance it only when the underlying ACH entry settles – delayed by 1–2 banking days, like all ACH – so an OPEN invoice with a pending bank payment is normal, not stuck.
| Field | Type | Description |
|---|---|---|
DRAFToptional | status | Created but not sent; freely editable. |
OPENoptional | status | Sent to the customer; awaiting payment on the hosted page. |
PARTIALoptional | status | Partially paid; paid shows cents collected so far. |
PAIDoptional | status | Paid in full. Fires invoice.paid. |
VOIDoptional | status | Voided. Terminal. |
How do you know it was paid? Listen for invoice.paid – it fires when the invoice reaches PAID, including when a delayed ACH payment finally settles. invoice.created fires on create. Polling GET /invoices/{id} works too.
| Field | Type | Description |
|---|---|---|
customer.namerequired | string | Bill-to name. |
customer.email / phoneoptional | string | Contact (email needed to send). |
customer.address1 / city / state / zipoptional | string | Bill-to address. |
line_items[]required | array | One or more line items (below). |
line_items[].descriptionrequired | string | Line description. |
line_items[].quantityoptional | number | Quantity. Default 1. |
line_items[].unit_pricerequired | integer | Unit price in cents. |
tax_rate_bpsoptional | integer | Invoice-level tax in basis points. |
due_dateoptional | string | Due date, YYYY-MM-DD. |
invoice_dateoptional | string | Issue date, YYYY-MM-DD. Default today. |
notesoptional | string | Customer-facing note. |
sendoptional | boolean | Email the invoice on create. |
curl -X POST https://www.merchant360.net/api/m360/v1/invoices \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"customer": {
"name": "Jane Doe",
"email": "jane@example.com"
},
"line_items": [
{
"description": "Consulting",
"quantity": 2,
"unit_price": 15000
}
],
"tax_rate_bps": 875,
"due_date": "2026-08-01",
"send": true
}'Estimates
/estimatesCreate an estimate (identical body to invoices)./estimatesList estimates./estimates/{id}/acceptMark accepted (customer said yes off-platform). Add "convert": true to also turn it into an invoice.Estimates share the invoice shape – same body, same line items, same hosted page – but come back as object: "estimate". The customer reviews and accepts the hosted estimate, which converts it to a normal invoice; from that point it follows the invoice lifecycle (and its webhooks) exactly.
curl -X POST https://www.merchant360.net/api/m360/v1/estimates \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"customer": {
"name": "Jane Doe"
},
"line_items": [
{
"description": "Project",
"quantity": 1,
"unit_price": 250000
}
],
"send": true
}'Credit memos
A credit memo credits against an invoice. It comes in three kinds – writeoff, adjustment, chargeback – and applying is a separate step from creating: create a draft and apply it later with POST /credit-memos/{id}/apply, or pass apply: true to do both at once. Applying reduces the invoice balance immediately; voiding a memo reverses its effect.
/credit-memosCreate a credit memo./credit-memosList credit memos./credit-memos/{id}/applyApply the memo to an invoice./credit-memos/{id}/voidVoid (reverse) the memo.| Field | Type | Description |
|---|---|---|
invoice_idrequired | string | The invoice to credit. |
amountrequired | integer | Credit amount in cents (> 0). |
kindoptional | string | writeoff, adjustment (default), or chargeback. |
reasonoptional | string | Internal reason (≤500 chars). |
applyoptional | boolean | Apply immediately (vs. leave as a draft). |
curl -X POST https://www.merchant360.net/api/m360/v1/credit-memos \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"invoice_id": "INVOICE_ID",
"kind": "adjustment",
"amount": 500,
"reason": "Goodwill",
"apply": true
}'Payment links
A payment link is a fixed-amount hosted pay page at merchant360.net/pay/{token} – share the returned url by email, SMS, or embed. For itemized bills use invoices (each has its own hosted URL). For an embeddable, multi-mode checkout (donations, save-a-card, look-up-my-invoice),use the hosted pay widget.
/payment-linksCreate a link (amount 0 = payer chooses)./payment-linksList links./payment-links/{id}Retrieve one link./payment-links/{id}/textText (SMS) the link to the customer – STOP-compliant; pending links only.A link is born pending and ends as paid, cancelled, or expired. It becomes paid the moment the customer completes payment on the hosted page – a card payment flips it instantly, a bank (eCheck) payment flips it when the ACH entry settles. payment_link.paid fires on the transition (the event data is the same object GET /payment-links returns); links tied to an invoice also fire invoice.paid. Expiry (expires_in_days) is evaluated lazily: the status flips to expired when the link is next accessed after its deadline, not on a background timer, so an untouched link can read pending past its expiry until someone opens it.
| Field | Type | Description |
|---|---|---|
amountrequired | integer | Amount to collect, in cents (> 0). |
descriptionoptional | string | Shown on the pay page (≤200 chars). |
expires_in_daysoptional | integer | Auto-expire the link after N days. |
customer.name / email / phoneoptional | string | Pre-fill the payer's details. |
curl -X POST https://www.merchant360.net/api/m360/v1/payment-links \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 4999,
"description": "Deposit",
"expires_in_days": 30
}'Pay Widget
The Pay Widget is a drop-in, embeddable checkout: paste the two-line snippet into any website and it renders a complete, branded payment form in an iframe served from cygma.cloud/{merchantId}. Unlike payment links (single-use URLs) or Checkout Sessions (server-created, cart-shaped), the widget is a permanent fixtureon the merchant’s own page – same embed, unlimited payments. If you only want secure card fields inside your own UI, use hosted fields instead.
Everything except the four data- attributes is configured in Merchant360 → Pay Widget – enabled modes, min/max amounts, brand color, receipt addresses, membership programs – and is read server-side on every render, so config changes take effect immediately without touching the embed. The iframe auto-sizes (a cygma:resize postMessage), shows the receipt inline on success, and emails the customer a PDF receipt. The hosted form is protected by Cloudflare Turnstile, a honeypot, and per-IP velocity limits.
| Field | Type | Description |
|---|---|---|
data-merchantrequired | string | Your merchant UUID (shown on the Pay Widget page). |
data-moderequired | string | One of the modes below (must be enabled for your account). |
data-amountoptional | integer | Cents – FIXED_AMOUNT mode only. |
data-descriptionoptional | string | Shown on the form and the receipt. |
| Field | Type | Description |
|---|---|---|
FIXED_AMOUNToptional | mode | You set the amount; the customer just pays. |
DONATIONoptional | mode | Preset amount chips (data-suggested, whole dollars) + custom amount within your min/max. |
EXTERNAL_INVOICEoptional | mode | Customer types your invoice number + amount (pre-fill with ?invoice= and ?memo=). |
M360_INVOICEoptional | mode | Look-up-my-invoice: customer finds their open Merchant360 invoice by number + email and pays the balance. |
SUBSCRIPTIONoptional | mode | Customer joins a membership program you define (first charge now, card vaulted, auto-bills on schedule). |
SAVE_CARDoptional | mode | Vault a card without charging – temporarily unavailable; contact EPI to enable card-on-file. |
Server-to-server: the widget also has a REST endpoint for merchants who collect card details in their own backend – POST https://www.cygma.cloud/api/cygma/v1/charge, authenticated with the widget’s cyg_… bearer key (a separate key from your m360_ API keys: minted, rotated, and revoked on the Pay Widget page, shown once at issuance). It accepts the FIXED_AMOUNT, DONATION, and EXTERNAL_INVOICE modes; invoice-lookup and subscription enrollment are hosted-flow only. Failures return 401 (bad key), 400 (validation), or 402 (processing failure).
| Field | Type | Description |
|---|---|---|
moderequired | string | FIXED_AMOUNT | DONATION | EXTERNAL_INVOICE. |
amountCentsrequired | integer | 100 – 100,000,000 (must fit your configured min/max). |
customerrequired | object | { name, email, phone? } – the receipt goes to this email. |
cardrequired | object | { number, expMonth, expYear, cvv, postalCode, street1?, city?, state? }. |
descriptionoptional | string | Shown on the receipt. |
externalInvoiceRefoptional | string | EXTERNAL_INVOICE – your invoice number (search-indexed in M360). |
externalInvoiceMemooptional | string | EXTERNAL_INVOICE – freeform memo. |
How do you know it worked? Widget payments confirm synchronously – the inline receipt (hosted) or the {ok:true} response (API) – and every successful charge also fires payment.succeeded (a full look-up-my-invoice payment additionally fires invoice.paid), so widget activity lands in the same webhook feed as your API charges. Refunds and the fraud ledger live on the Merchant360 → Pay Widget page.
<!-- Drop-in checkout: two lines on any page. -->
<div data-cygma-pay
data-merchant="YOUR_MERCHANT_UUID"
data-mode="FIXED_AMOUNT"
data-amount="2500"
data-description="Invoice #1042"></div>
<script src="https://cygma.cloud/pay-widget.js" async></script>Checkout Sessions
A Checkout Session is a fully Cygma-hosted payment page. Create one server-side, then redirect the customer to the returned url. They pay on our page; we charge, mark the session complete, fire checkout.session.completed, and redirect to your success_url. Pass either an amount or a line_items array (their sum becomes the total). Unlike a payment link, a session is single-use and redirect-oriented (success/cancel URLs + a completion webhook).
A session is open until it becomes complete (paid) or expired. Completion is an atomic, single-use claim: exactly one charge can ever complete a session, so a double-submit or a customer paying from two tabs cannot pay twice – and if the charge itself fails, the session reopens so the customer can try again. Expiry (expires_in_minutes, default 24 hours, max 30 days) is evaluated lazily on access, like payment links.
Pass the customer when you have them. An optional customer object – { name, email, phone } – pre-fills the hosted form and powers wallet features: the phone is the buyer’s cell, used as the text-message verification anchor when they save their card with Pay with Vault (absent a phone, the hosted page collects one under the consent box).
Fulfil on the webhook, not the redirect. A customer can pay and close the tab before ever reaching your success_url – the redirect is a courtesy; checkout.session.completed is the truth. Verify the signature, look up the session by id, then ship the order.
/checkout/sessionsCreate a session; returns the hosted url to redirect to./checkout/sessionsList sessions./checkout/sessions/{id}Retrieve one – poll for status/payment (or use the webhook).| Field | Type | Description |
|---|---|---|
amountoptional | integer | Total to collect, in cents. Required unless line_items is given. |
line_itemsoptional | object[] | [{ name, amount (cents), quantity }]. Their sum is the total. |
success_urlrequired | string | Redirect after payment. {CHECKOUT_SESSION_ID} is substituted; otherwise session_id is appended. |
cancel_urloptional | string | Where the customer returns if they cancel. |
descriptionoptional | string | Shown on the hosted page (≤300 chars). |
customer.name / emailoptional | string | Pre-fill the payer's details. |
customer.phoneoptional | string | The buyer's cell. Powers Pay with Vault: it's the text-message verification anchor for saving and re-using cards. Pass it whenever you have it (e.g. the billing phone from your order form). |
client_reference_idoptional | string | Your own id, echoed back on the session + webhook. |
metadataoptional | object | Arbitrary key/values echoed back. |
expires_in_minutesoptional | integer | Auto-expire the session (default 1440 = 24h). |
Gift cards. When the merchant runs an eGift program, the hosted page also offers “Have a Merchant360 gift card?” – the payer can cover part or all of the session with stored value, and any card surcharge computes on the card portion only. The completed session itemizes it: amount_gift (stored-value portion, 0 when none), amount_charged (total collected = gift + card incl. surcharge), and payment.gift ({ last4, amount, transaction_id }) on the object and the checkout.session.completed webhook. A session fully covered by a gift card completes with no card at all (payment.card.last4 null). Refunding a gift-paid order? Void the redemption with POST /egift/void using payment.gift.transaction_id.
Test-mode keys create is_test sessions: card 4242 4242 4242 4242 approves, 4000 0000 0000 0002 declines, and any test gift card carries a $25.00 balance (ending 0000 = not found) – no money moves. Listen for checkout.session.completed (or poll the session) to fulfil the order.
▶ Try the live demo store – click Buy, pay on the hosted page, land back with the result.
Pay with Vault on your checkout
Every hosted session automatically offers Pay with Vault – the Merchant360 network wallet. A returning customer types the email or mobile number on their wallet, receives a one-time text, and pays with a saved card in seconds; a new customer can save their card at payment with one checkbox. There is no integration work: it appears on the redirect flow, the popup button, WooCommerce, and the Pay Widget – anywhere a session renders.
Two things to know as an integrator: (1) pass customer.phone when you have it – verification codes are delivered by text message; without a phone on the session, the hosted page asks the payer for their cell under the save-my-card box. (2) The wallet lives on the hosted page only – there are no API endpoints to look up, verify, or charge a customer's Vault wallet. Sessions on surcharge programs, open-amount links, gift-covered payments, and test mode skip the wallet.
Checkout Button (popup)
Prefer the customer to stay on your page, PayPal-style? Load m360-checkout.js and the same session opens in a centered popup; when the customer pays, the popup closes and your onComplete fires (closing the popup without paying fires onCancel). Your backend still creates the session with your secret key – the snippet either takes a pre-created url or calls a sessionEndpoint on your server that returns {"url": …}.
<script src="https://cygma.cloud/m360-checkout.js"></script>
<button id="pay-btn">Pay with Merchant360</button>
<script>
M360Checkout.attach('#pay-btn', {
sessionEndpoint: '/api/create-checkout-session', // your backend: POST /v1/checkout/sessions, respond {"url": session.url}
onComplete: function (r) {
// UX signal only — confirm via the checkout.session.completed webhook before fulfilling.
window.location.href = '/thanks';
},
onCancel: function () { /* customer closed the popup */ },
});
</script>As with the redirect flow, fulfil on the webhook, not the callback – onComplete can be spoofed by a hostile browser; checkout.session.completed cannot.
Inline (embedded iframe)
Want the pay surface embedded on your page rather than a popup or a redirect? Render the session url in an iframe with ?embed=1. The framed page keeps itself sized by posting { type: 'm360:checkout:resize', height } to window.parent, and on success posts { type: 'm360:checkout:complete', nonce, token } to window.parent (no redirect happens inside the frame – your page decides what to do next). Pass a random pnonce and check it echoes back. The card is entered inside this cross-origin frame.
const pnonce = crypto.randomUUID();
const iframe = document.createElement('iframe');
iframe.src = session.url + '&embed=1&pnonce=' + pnonce; // session.url from POST /v1/checkout/sessions
iframe.style.width = '100%'; iframe.style.border = '0';
document.querySelector('#pay').appendChild(iframe);
const origin = new URL(session.url).origin;
window.addEventListener('message', (e) => {
if (e.origin !== origin) return;
if (e.data.type === 'm360:checkout:resize') iframe.style.height = e.data.height + 'px';
if (e.data.type === 'm360:checkout:complete' && e.data.nonce === pnonce) {
// UX signal only — confirm via the checkout.session.completed webhook before fulfilling.
window.location.href = '/thanks';
}
});This is the mode the Merchant360 for WooCommerce plugin uses for its inline checkout. Same rule: fulfil on the webhook, not the message.
curl -X POST https://www.merchant360.net/api/m360/v1/checkout/sessions \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 4999,
"description": "Order #1234",
"success_url": "https://yourapp.com/thanks?session_id={CHECKOUT_SESSION_ID}",
"cancel_url": "https://yourapp.com/cart"
}'Card-Present Terminal
For merchants with a paired Handpoint card-present reader, push a sale to the physical terminal instead of keying the card: the customer taps or inserts on the device, EMV data goes straight from the reader to the gateway, and no card data ever crosses this API – you exchange amounts and references only.
The flow is asynchronous, because a human has to present a card: create the charge (returns immediately with status: "pending"), then poll GET /terminal/charges/{id} every 2–3 seconds until it resolves to approved, declined, or cancelled (the customer can cancel on the device). An approved result carries the card brand/last4, the approved amount (tip-inclusive if you enabled the on-reader tip prompt with enable_tip), the auth code, and a transaction_guid for later tip-adjust/void in Merchant360.
/terminal/devicesList paired readers. available:false = hide the option./terminal/chargesPush a sale to a reader (async – returns pending)./terminal/charges/{id}Poll the outcome: pending → approved / declined / cancelled. `raw_status` echoes Handpoint's own outcome word for diagnostics./terminal/charges/{id}/receiptEmail the customer a branded receipt for an approved sale – body { email, name? }. Same receipt every Merchant360 surface sends. (Card payments have the matching POST /payments/{id}/receipt.)Omit terminal_id to use the account's default reader. Send an Idempotency-Key (your order id) so a network retry can't push the sale to the reader twice. Test-mode keys get a simulated reader (TEST/SIM0001) whose charges approve instantly on the first poll. This is the same rail the Payment Extension's “Send to card terminal” button uses.
curl -X POST https://www.merchant360.net/api/m360/v1/terminal/charges \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"amount": 4999,
"terminal_id": "PAXA920/1850012345",
"reference": "Order #1234"
}'Subscriptions
Recurring billing with trials, discounts, usage-based pricing, and end conditions. The example creates a flat monthly plan; usage-based pricing is set via pricing (below). ▶ Try the live demo.
A subscription is active from the moment you create it. A trial does not get its own status – it’s just an active subscription whose first charge is deferred to trial end. A daily billing cron charges everything due, and each successful charge advances next_charge_at on submit. When a card charge declines, the subscription enters the dunning retry ladder and becomes past_due; a successful retry returns it to active, while exhausting the ladder pauses or cancels it per the merchant’s dunning settings. When an end condition is reached (ends.after occurrences, ends.on date, or ends.at_amount cap), it completes naturally as ended.
| Field | Type | Description |
|---|---|---|
activeoptional | status | Billing normally (includes trials – first charge deferred to trial end). |
past_dueoptional | status | A charge declined; the dunning retry ladder is working the balance. |
pausedoptional | status | No charges until resumed – set manually or by ladder exhaustion. |
cancelledoptional | status | Stopped – manually or by ladder exhaustion. Fires subscription.canceled. |
endedoptional | status | Natural completion: occurrence count, end date, or amount cap reached. |
failedoptional | status | Billing stopped after unrecoverable failure. |
subscription.created and subscription.canceled fire on those transitions; each cycle’s charge fires the normal payment webhooks. ACH-billed subscriptions inherit ACH’s delayed settlement – a cycle’s debit is queued on the charge date and settles 1–2 banking days later.
| Field | Type | Description |
|---|---|---|
plan_namerequired | string | Name of the plan/subscription. |
intervalrequired | string | Billing cadence: weekly, biweekly, monthly, quarterly, semiannual, annual, or custom. |
interval_daysoptional | integer | Days between charges when interval is custom. |
fire_dayoptional | integer | For weekly, day of week to charge (0=Sun … 6=Sat). |
amountoptional | integer | Per-cycle amount in cents. Required for flat pricing; ignored for usage-based. |
start_atoptional | string | First charge date, YYYY-MM-DD. Default today. |
customer_idoptional | string | Attach to a customer. |
| Field | Type | Description |
|---|---|---|
payment_methodoptional | string | card (default) or ach. |
vault_tokenoptional | string | Required for card plans – obtain from a payment with save_card: true. |
| Field | Type | Description |
|---|---|---|
trial_daysoptional | integer | Free-trial length; the first recurring charge is deferred by this many days. |
setup_feeoptional | integer | One-time fee in cents, billed on the FIRST card charge only (it rides that charge, then never recurs). Ignored for ach – a signed authorization covers a single recurring amount. |
discount.percent_offoptional | number | Percent off every cycle (e.g. 10 = 10%). |
discount.amount_offoptional | integer | Flat amount off every cycle, in cents. Use one of percent_off / amount_off. |
| Field | Type | Description |
|---|---|---|
pricing.modeoptional | string | flat (default, uses amount), per_unit, or tiered. |
pricing.unit_priceoptional | integer | Cents per unit (per_unit mode). |
pricing.unit_labeloptional | string | Unit name, e.g. seat. |
pricing.tiers[]optional | array | Tiered pricing (tiered mode): [{ up_to, unit_price }]. Use up_to: null for the final open tier. |
| Field | Type | Description |
|---|---|---|
ends.afteroptional | integer | Stop after N charges. |
ends.onoptional | string | Stop on a date, YYYY-MM-DD. |
ends.at_amountoptional | integer | Stop once this total (cents) has been billed. |
Manage
/subscriptionsList subscriptions./subscriptions/{id}Retrieve a subscription./subscriptions/{id}Update status: { "status": "pause" } – accepts active, pause, cancel./subscriptions/{id}/chargeCharge off-cycle now.curl -X POST https://www.merchant360.net/api/m360/v1/subscriptions \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"plan_name": "Pro monthly",
"amount": 2999,
"interval": "monthly",
"payment_method": "card",
"vault_token": "VAULT_TOKEN",
"trial_days": 14,
"setup_fee": 500,
"discount": {
"percent_off": 10
},
"ends": {
"after": 12
}
}'Appointments
/appointmentsBook an appointment./appointmentsList appointments (?from= & ?to=)./appointments/{id}Retrieve an appointment./appointments/{id}Reschedule / update./service-typesList bookable service types (durations, prices, colors, buffers)./staffList bookable staff members (Scheduling v2 – includes name-only staff; resolve an appointment's assignee.staff_id here).Appointment status values are the lowercase lifecycle – scheduled → confirmed → in_progress → completed, with canceled and no_show as exits – and you drive every transition yourself via PATCH; nothing advances automatically. Booking a slot the assignee already occupies returns 409 conflict; pass force: true to double-book deliberately.
| Field | Type | Description |
|---|---|---|
titlerequired | string | What the appointment is. |
start_atrequired | string | ISO datetime. |
end_atrequired | string | ISO datetime, after start. |
service_type_idoptional | string | A service from GET /service-types. |
customer_idoptional | string | Attach to an existing customer. |
customer_name / email / phoneoptional | string | Inline customer details. |
assigned_user_idoptional | string | Staff member assigned. |
assignee_nameoptional | string | Free-text assignee name. |
locationoptional | string | Location/address text. |
priceoptional | integer | Price in cents. |
notesoptional | string | Internal notes. |
forceoptional | boolean | Book even if the assignee is already busy (else 409). |
| Field | Type | Description |
|---|---|---|
statusrequired | string | scheduled, confirmed, in_progress, completed, canceled, no_show. |
curl -X POST https://www.merchant360.net/api/m360/v1/appointments \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"title": "Install",
"start_at": "2026-08-01T14:00:00Z",
"end_at": "2026-08-01T15:00:00Z",
"customer_name": "Jane Doe",
"assigned_user_id": "STAFF_USER_ID",
"price": 12000
}'Classes
Capacity-capped group sessions – free RSVPs or paid bookings – shown on the merchant’s public booking page and anywhere the booking widget embeds. A class is the template (title, capacity, pricing); a class session is a dated occurrence; a class bookingis one attendee’s seat. Seats are claimed atomically across API and public bookings.
/classesList classes (?active= filter)./classesCreate a class./classes/{id}Retrieve a class./classes/{id}Update (partial). Deactivate with active: false – classes with booking history are never deleted./class-sessionsList sessions with spots_left (?class_id=, ?from=, ?to=)./class-sessionsSchedule sessions – repeat_weeks creates a weekly series./class-sessions/{id}Retrieve a session./class-sessions/{id}Cancel / reinstate (status: canceled | scheduled)./class-sessions/{id}/bookingsSession roster (all statuses)./class-sessions/{id}/bookingsAdd an attendee (walk-in / phone booking)./class-bookings/{id}Retrieve a booking./class-bookings/{id}Remove an attendee – frees the seat; the waitlist promotes.Payments: API bookings do not collect payment – the public /book page owns card collection (full price or deposit at booking, with Vault quick-pay). Adding an attendee over the API registers them with paid: 0. Public-side require_email / require_phonerules don’t apply to API bookings.
Full sessions: POST /class-sessions/{id}/bookings returns 409 sold_out when no seats remain – pass waitlist: trueto join the waitlist instead. Removing a confirmed attendee promotes the earliest waitlisted person automatically: free classes auto-confirm them (with a “you’re in” email); paid classes email a first-come booking link. Session cancels over the API do not email the roster – cancel from the app to notify attendees.
| Field | Type | Description |
|---|---|---|
titlerequired | string | Class name (≤120 chars). |
descriptionoptional | string | Shown in the public booking modal. |
instructor_staff_idoptional | string | A staff id from GET /staff. |
capacityoptional | integer | Max people per session. Default 10. |
payment_modeoptional | string | free (RSVP only), full (pay price to book), deposit (pay deposit to book). Default free. |
priceoptional | integer | Cents. Required > 0 for full / deposit. |
depositoptional | integer | Cents due at booking (deposit mode); can't exceed price. |
coloroptional | string | #rrggbb calendar color. |
require_email / require_phoneoptional | boolean | Public bookings must include an email / mobile number. |
activeoptional | boolean | Bookable on the public calendar. Default true. |
| Field | Type | Description |
|---|---|---|
class_idrequired | string | The class to schedule. |
start_atrequired | string | ISO datetime. |
duration_minoptional | integer | Default 60. |
capacity_overrideoptional | integer | Override the class capacity for these sessions. |
repeat_weeksoptional | integer | 1–52; >1 creates a weekly series sharing a series_id. |
| Field | Type | Description |
|---|---|---|
namerequired | string | Attendee name. |
emailoptional | string | Attendee email (used for waitlist promotion emails). |
phoneoptional | string | Attendee mobile number. |
marketing_opt_inoptional | boolean | Adds them to the class marketing list export. |
waitlistoptional | boolean | Join the waitlist instead of failing with 409 sold_out when full. |
curl -X POST https://www.merchant360.net/api/m360/v1/classes \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"title": "Beginner Pottery Wheel",
"description": "Two hours on the wheel – clay and tools provided.",
"capacity": 8,
"payment_mode": "full",
"price": 4500,
"color": "#1d4ed8"
}'Staff shifts
The merchant’s employee work schedule (Team Schedule) – the same rows the Merchant360 grid and the TableTurn shift schedule edit. A shift belongs to one worker: a login team member (u:<user_id>) or a register PIN (c:<cashier_id>); worker_key: null is an open shift nobody has claimed. New shifts are drafts until published; staff only ever see published shifts.
/shiftsList shifts in a window (?from=, ?to=, ISO; default this week + next). Includes the roster of worker keys./shiftsCreate a draft shift./shifts/{id}Retrieve a shift./shifts/{id}Update (partial): times, break_min, role, note, or reassign via worker_key./shifts/{id}Remove a shift./shifts/publishPublish every draft in { from, to }. On a Team Schedule Plus merchant each scheduled worker is texted./schedule-requestsStaff time-off, drop, pick-up and handoff requests (?status=pending). Plus feature./schedule-requests/{id}Decide: { decision: "approve" | "deny", note? }. The worker is texted. A handoff the coworker has not accepted yet returns 409.| Field | Type | Description |
|---|---|---|
worker_keyoptional | string | null | u:<user_id> or c:<cashier_id> from the roster; null = open shift. |
starts_at / ends_atoptional | ISO datetime | End after start, at most 24 hours. |
break_minoptional | integer | Unpaid break minutes, netted out of hours and labor cost. |
roleoptional | string | Position label shown to the worker (Server, Front desk). |
statusoptional | draft | published | Read-only; flip with POST /shifts/publish. |
curl -X POST https://www.merchant360.net/api/m360/v1/shifts \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"worker_key": "c:6f1c…",
"starts_at": "2026-09-02T14:00:00Z",
"ends_at": "2026-09-02T22:00:00Z",
"break_min": 30,
"role": "Server"
}'Reports
| Field | Type | Description |
|---|---|---|
fromoptional | string | ISO datetime start. Default 30 days ago. |
tooptional | string | ISO datetime end. Default now. |
{ "object": "report_summary", "period": { "from": "…", "to": "…" }, "gross_sales": 128400, "transaction_count": 37, "currency": "usd" }Statements
Download the merchant’s own processing statement as a branded PDF. GET /statements lists the periods a statement exists for (newest first); GET /statements/pdf?year=YYYY&month=MM streams that month’s PDF. The statement is always the merchant behind the key – there’s no cross-merchant access.
| Field | Type | Description |
|---|---|---|
yearrequired | integer | Four-digit year, e.g. 2026. |
monthrequired | integer | 1–12. |
/statementsList available statement periods (period, year, month, pdf_url)./statements/pdfDownload the statement PDF for a period (application/pdf). 404 if none.curl -X GET https://www.merchant360.net/api/m360/v1/statements \ -H "Authorization: Bearer m360_live_xxx"
Settings
Read the merchant’s billing settings that affect the API – accepted methods, ACH direction policy, dual pricing, surcharge program, split-into-payments (installments) config, and business/legal profile. Any valid key can read its own merchant.
Surcharge. surcharge.enabled + surcharge.percent describe a credit-card surcharge program (mutually exclusive with dual_pricing). Use it to display the surcharge to shoppers. Whether a specific card is actually surcharged (credit yes, debit no, and any code-97 drop) is decided by BIN on the hosted pay surface at charge time, so the amount finally charged may differ from a naive amount × percent – reconcile to the charged total, don’t assume it.
Updating settings. PATCH /settings (scope settings:write) changes the two blocks the merchant controls in-app – payments (accepted methods) and installments. It’s a partial merge, so send only what changes. Pricing programs (dual pricing / surcharge), the business profile, and account capabilities are compliance/underwriting-controlled and are not API-writable. Enabling accept_bank_ach requires the account’s ACH capability (else 422).
{ "object": "settings",
"business": { "dba_name": "Acme Co", "legal_name": "Acme Inc", "email": "…", "phone": "…", "address": {…}, "legal_address": {…} },
"payments": { "accept_card": true, "accept_bank_ach": true, "accept_cash_check": false, "ach_allow_debit": true, "ach_allow_credit": false },
"dual_pricing": { "enabled": true, "card_adjustment": 399 },
"surcharge": { "enabled": false, "percent": 0, "credit_only": true },
"installments": { "enabled": true, "min_cents": 5000, "max_payments": 4, "interval_days": 30 } }Custom fields
Custom fields collect extra info at pay time (a PO number, a policy #, a table). Define a field once and it renders on the payer surfaces you enable, plus the staff virtual terminal. Captured values come back as metadata on the payment – and you can also set metadata directly on a charge without defining a field.
/custom-fieldsList field definitions./custom-fieldsCreate a field./custom-fields/{id}Retrieve a field./custom-fields/{id}Update a field./custom-fields/{id}Remove a field.| Field | Type | Description |
|---|---|---|
labelrequired | string | Shown to the payer (≤60 chars). The stable key is derived from it. |
typeoptional | string | "text" (default) or "select". |
optionsoptional | string[] | Choices for a "select" field (2–25). |
requiredoptional | boolean | Whether the payer must fill it in. |
surfacesoptional | object | Where it renders – payment_links, invoices, estimates, checkout, pay_widget, virtual_terminal (each defaults to true). |
receiptsoptional | object | Where the captured value prints – customer_receipt (default false), merchant_receipt (default true). |
accounting_classoptional | boolean | "select" fields only. The captured option posts as the QuickBooks Online Class on synced invoice lines and settlement journal lines, and as the Xero tracking option (category named after the field’s label). One field per merchant – setting it clears the flag on the others. |
Filter GET /payments by a captured value with metadata[<key>]=<value> (for example ?metadata[payment_for]=Golf%20Outing). Several keys combine with AND.
curl -X POST https://www.merchant360.net/api/m360/v1/custom-fields \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"label": "PO Number",
"type": "text",
"required": true,
"surfaces": {
"payment_links": true,
"invoices": true
}
}'Buy Now, Pay Later
Offer Affirm and Klarna at checkout. BNPL is a two-step, widget-based flow – the payer authorizes in the provider’s UI, which yields a token you capture into a charge:
- Create a session –
POST /bnpl/sessionsfor an amount. Klarna returns aclient_tokenfor the Klarna SDK; Affirm returns apublic_key+script_urlfor affirm.js (Affirm has no server session). - Render the widget on your checkout with that token/key. The payer authorizes and the widget hands you a token – Affirm a
checkout_token, Klarna anauthorization_token. - Capture –
POST /bnpl/chargeswith that token. We authorize + capture at the provider and book the charge. Passinvoice_idto also mark a Merchant360 invoice paid.
Settlement is outside the card rails. Affirm and Klarna deposit directly to the merchant’s bank, so a BNPL charge is its own resource, separate from /payments, and it does not appear in card reporting. A test key uses the provider sandbox; a live key cannot refund a test charge.
| Field | Type | Description |
|---|---|---|
providerrequired | string | "affirm" or "klarna". |
amountrequired | integer | Cents (>0). Affirm's practical floor is $50. |
invoice_numberoptional | string | Shown on the Klarna order line. Defaults to "API". |
customer.emailoptional | string | Optional – passed to Klarna as the billing email. |
| Field | Type | Description |
|---|---|---|
providerrequired | string | "affirm" or "klarna". |
amountrequired | integer | Cents (>0) – must match the amount the widget authorized. |
tokenrequired | string | Affirm checkout_token / Klarna authorization_token from the widget. |
invoice_idoptional | string | Optional – a Merchant360 invoice to mark paid with the same bookkeeping as the pay page. |
customeroptional | object | Optional name + email stored on the charge. |
/bnpl/sessionsStart a checkout. Returns client_token (Klarna) or public_key + script_url (Affirm) for the widget./bnpl/chargesCapture the widget's token into a charge; optionally mark an invoice paid. Idempotent./bnpl/chargesList BNPL charges (newest first, paginated)./bnpl/charges/{id}Fetch a single charge./bnpl/charges/{id}/refundRefund full (omit amount) or partial. Un-books any invoice it paid. Idempotent.curl -X POST https://www.merchant360.net/api/m360/v1/bnpl/charges \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"provider": "klarna",
"amount": 12000,
"token": "authorization_token_from_widget",
"invoice_id": "inv_…"
}'Vendor Payments
Track accounts-payable vendors, record the payments you make them (check / cash / ACH done elsewhere), and produce 1099-NEC recipient copies at year end. The API records payments and books the P&L expense – it does not disburse money (real ACH payout stays in-app, owner-only).
TIN and bank handling: send a payee’s tax_id and bank_account in plaintext; both are encrypted at rest and only the last-4 is ever returned. Storing a bank does not pay it – real ACH disbursement stays in-app and owner-only.
| Field | Type | Description |
|---|---|---|
namerequired | string | Vendor legal / DBA name. |
entity_typeoptional | string | BUSINESS (default) or INDIVIDUAL – drives SSN vs EIN on the 1099. |
email / phoneoptional | string | Contact info. |
addressoptional | object | line1, line2, city, state, zip – required to file a 1099. |
tax_idoptional | string | 9-digit SSN/EIN (encrypted at rest; last-4 returned). |
is_1099_vendoroptional | boolean | Whether this vendor is 1099-reportable (default true). |
bank_accountoptional | object | routing_number (9 digits, checksum-validated), account_number (4–17 digits), type – CHECKING (default) or SAVINGS. Send the routing and account together: either alone leaves the stored bank unchanged. Write-only – reads return bank_on_file with the last-4 only. |
| Field | Type | Description |
|---|---|---|
payee_idrequired | string | The payee to pay. |
amountrequired | integer | Cents (>0). |
memooptional | string | Free-text note (shown on the P&L expense). |
payment_dateoptional | string | YYYY-MM-DD; defaults to today. Determines the 1099 tax year. |
reportable_1099optional | boolean | Counts toward the payee's 1099 total. Default true. |
A payee gets a 1099-NEC when they’re a 1099 vendor and their reportable total for the year is ≥ $600. GET /vendor-payments/1099s?year=YYYY lists who qualifies with a ready flag (and missing listing TIN/address gaps); the recipient-copy PDF is GET /vendor-payments/payees/{id}/1099.
/vendor-payments/payeesList payees (with this-year totals)./vendor-payments/payeesCreate a payee./vendor-payments/payees/{id}Get a payee./vendor-payments/payees/{id}Update a payee (partial; blank tax_id keeps the stored TIN, absent bank_account keeps the stored bank)./vendor-payments/payees/{id}Archive a payee./vendor-payments/payees/{id}/w9W-9 PDF – ?type=blank (prefilled) or signed (stored)./vendor-payments/payees/{id}/10991099-NEC recipient copy (PDF). ?year=YYYY./vendor-payments/paymentsRecord a payment (books the P&L expense). Idempotent./vendor-payments/paymentsList payments. ?payee_id= to filter./vendor-payments/payments/{id}Get a payment./vendor-payments/1099s1099 rollup for a tax year (JSON).curl -X POST https://www.merchant360.net/api/m360/v1/vendor-payments/payments \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"payee_id": "…",
"amount": 150000,
"memo": "October retainer",
"payment_date": "2026-10-01"
}'TIN Match
Verify a legal name + TIN (SSN or EIN) against the IRS TIN Match service before 1099 filing. This scope is granted by EPI (it is not available in the self-serve sandbox).
| Field | Type | Description |
|---|---|---|
namerequired | string | The legal name on the TIN (business or individual). |
tinrequired | string | 9-digit SSN or EIN (dashes optional). |
tin_typeoptional | string | "SSN" / "EIN" / "ITIN". Omit and the IRS resolves which file to check. |
| Field | Type | Description |
|---|---|---|
objectoptional | string | Always "tin_match". |
matchedoptional | boolean | true only on an exact name + TIN match (response codes 0, 6, 7, 8). |
verdictoptional | string | MATCH / MISMATCH / NOT_ISSUED / INVALID_INPUT / DUPLICATE / ERROR. |
response_codeoptional | number | null | The raw IRS numeric code (see the table below). null when the request never reached the IRS. |
descriptionoptional | string | Plain-language explanation of the code, safe to log or surface to staff. |
tin_last4optional | string | null | Last 4 digits of the submitted TIN, for your own reconciliation. The full TIN is never returned. |
sourceoptional | string | LIVE when the IRS was queried, CIRCUIT_OPEN if our rate-limit guard short-circuited the call, DISABLED if matching is turned off. |
checked_atoptional | string | ISO 8601 timestamp of the check. |
| response_code | verdict | matched | Meaning |
|---|---|---|---|
0 | MATCH | true | TIN and Name combination matches IRS records. |
1 | INVALID_INPUT | false | TIN is missing or not 9-digit numeric. |
2 | NOT_ISSUED | false | TIN entered is not currently issued by the IRS. |
3 | MISMATCH | false | TIN and Name combination does not match IRS records. |
4 | INVALID_INPUT | false | Invalid TIN Matching request. |
5 | DUPLICATE | false | Duplicate request – the IRS caps repeats per 24h; we cache to avoid tripping it. |
6 | MATCH | true | Matched on SSN (returned when tin_type is omitted/unknown). |
7 | MATCH | true | Matched on EIN (returned when tin_type is omitted/unknown). |
8 | MATCH | true | Matched on both SSN and EIN. |
Identical checks are cached for 24 hours: a repeat of the same name + TIN returns the cached verdict, with source reflecting the cache.
curl -X POST https://www.merchant360.net/api/m360/v1/tin-match \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme LLC",
"tin": "123456789",
"tin_type": "EIN"
}'Embedded Terminal
Drop the merchant’s full virtual terminalinto your product as an iframe: card + Check (ACH), dual pricing, the saved-card vault, receipts – every charge settles on the merchant’s own account. The merchant mints the key in Merchant360 → Billing Tools → Settings → Embedded terminal and registers the domains your product runs on.
<iframe
src="https://www.merchant360.net/embed/vt?key=vtk_…&header=1"
style="width:100%;min-height:960px;border:0"
></iframe>| Field | Type | Description |
|---|---|---|
keyrequired | string | The embed key. vtk_test_… keys load directly for development; live vtk_… keys only work inside a registered domain (Referer-checked before any session exists). |
headeroptional | string | 1 shows the merchant’s name above the terminal. OFF by default – the embed ships clean, with only a “Payments powered by Merchant360” mark under the payment button. |
The key bootstraps a 30-minute origin-bound session cookie scoped to /embed;*.example.com domain entries match subdomains. Keys can be revoked at any time. In Handpoint device mode (offered automatically when the merchant has provisioned readers) the cardholder taps/dips on the reader.
Getting the sales back: register a webhook and keep payment.succeeded events whose origin is EMBED_VT / EMBED_HP and whose embed_key_id matches your key – or poll GET /payments with ?origin=EMBED_VT&embed_key=<key id>. Sales keyed anywhere else (the in-app terminal, invoices, pay links) do not match this filter.
Webhooks
/webhooksRegister an endpoint (returns the signing secret, shown once)./webhooksList endpoints./webhooks/{id}Remove an endpoint.| Field | Type | Description |
|---|---|---|
urlrequired | string | HTTPS endpoint to receive events. |
eventsoptional | string[] | Event types to receive. Omit for all. |
Each delivery is a POST whose body is an event envelope – { "id": "evt_…", "object": "event", "type": "payment.succeeded", "created": "…", "merchant_id": "…", "data": { … } } – where data is the full object the event is about (a payment, an ach_payment, a checkout session, …), so you rarely need a follow-up GET. Two headers ride along:
M360-Signature: t=<unix>,v1=<hex>
M360-Event: payment.succeededVerify by computing HMAC_SHA256(secret, `${t}.{raw_body}`) with your whsec_ secret and comparing to v1 – sign the raw body bytes, before any JSON parsing.
Respond 2xx quickly and do your work async. Anything else – including a timeout – is retried with backoff at roughly 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, and 12 h, up to 8 total attempts. Delivery ordering is not guaranteed (a retried ach.queued can arrive after ach.settled), and retries mean you can see the same event twice – process events idempotently by id. Endpoints must be https.
| Field | Type | Description |
|---|---|---|
payment.succeededoptional | event | A card charge succeeded. |
payment.authorizedoptional | event | A hold was placed (pre-auth). |
payment.capturedoptional | event | A prior authorization was captured. |
payment.voidedoptional | event | A hold or unsettled charge was voided. |
payment.refundedoptional | event | A payment was refunded. |
card.createdoptional | event | A reusable card was vaulted (save_card / POST /tokens). |
invoice.createdoptional | event | An invoice was created. |
invoice.sentoptional | event | An invoice was sent to the customer (emailed or texted). |
invoice.paidoptional | event | An invoice was paid in full. |
invoice.voidedoptional | event | An invoice was voided. |
estimate.acceptedoptional | event | A customer e-signed and accepted an estimate. |
subscription.createdoptional | event | A subscription started. |
subscription.canceledoptional | event | A subscription was canceled. |
ach.queuedoptional | event | An ACH debit/credit was queued. |
ach.settledoptional | event | An ACH entry settled – funds moved. Your “funds are good” signal. |
ach.returnedoptional | event | The bank returned an ACH entry (payload carries return.code). |
payment_link.paidoptional | event | A payment link was paid (instantly by card; on settlement by bank). |
checkout.session.completedoptional | event | A customer paid a hosted Checkout Session. |
webhook.testoptional | event | A test event you fired from the dashboard (Send test event). |
Terminal-keyed sales: the data of a payment.succeeded fired by a virtual-terminal or Handpoint sale carries origin (EMBED_VT / EMBED_HP / M360_VT / M360_HP / ISO_VT) and embed_key_id – an embedding integration should keep events whose origin starts with EMBED_ and whose embed_key_id matches its own key, and drop the rest.
Every event payload includes a top-level merchant_id naming the merchant it belongs to – most useful for platform (ISV) webhooks, which receive events across many merchants on one endpoint.
▶ Try the live webhook inspector – fire signed events and watch them verify.
curl -X POST https://www.merchant360.net/api/m360/v1/webhooks \
-H "Authorization: Bearer m360_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://yourapp.com/hooks/m360",
"events": [
"payment.succeeded",
"invoice.created"
]
}'Zapier
Merchant360 for Zapierconnects your account to 7,000+ apps with no code: fire a Zap when a payment succeeds, an invoice is paid, a checkout completes, or an estimate is accepted — and create customers, invoices, payment links, and checkout sessions from any other app’s trigger.
Instant triggers(real webhooks, not polling): New Successful Payment, New Refund, New Invoice, Invoice Paid, Checkout Completed, Payment Link Paid, Estimate Accepted — plus a polling New Customer trigger. Actions: Create Customer, Create Invoice (optionally emailed with its hosted pay link), Create Payment Link, Create Checkout Session. Search: Find Customer.
Connect with your Merchant360 API key (sk_live_... or a sandbox sk_test_...— test keys run every Zap against simulated data). Under the hood each Zap registers a scoped endpoint via POST /webhooks and cleans it up when the Zap is turned off — nothing to configure here. The integration is in early access: ask your representative for an invite link while the public Zapier directory listing is in review.
Platform keys (ISV)
A platform key (m360_isv_…) lets a technology partner (ISV) act on behalf of the merchants in its own book through one integration, instead of collecting a separate key from every merchant.
Example.A POS company with 500 merchants gets ONE key. On each request it adds a header naming which of its merchants the call is for, and it can charge a card, save a card, send an invoice, pull a report, or receive webhooks for that merchant. The key can only reach merchants connected to the partner (by an explicit grant, or by being boarded under the partner’s office), and what it may do on each merchant is bounded by that merchant’s channel profile. Platform keys are issued by EPI or your ISO (not created in the sandbox).
Naming the merchant per request
Every merchant-scoped call (payments, tokens, invoices, …) takes an M360-On-Behalf-Of header naming the target merchant. The key can only reach a merchant it’s connected to – otherwise 403 no_grant. Everything else about the request is identical to a normal merchant key.
| Field | Type | Description |
|---|---|---|
M360-On-Behalf-Ofrequired | string | The target merchant’s id (from GET /accounts). Required on every merchant-scoped call with a platform key. |
Connected accounts
GET /accounts lists every merchant the key can act on. It’s partner-wide, so it takes no M360-On-Behalf-Of header.
| Field | Type | Description |
|---|---|---|
idoptional | string | The merchant id – pass it as M360-On-Behalf-Of. |
dba_nameoptional | string | The merchant's business name. |
linked_viaoptional | string | grant (explicit access) or boarded (boarded under the partner's office). |
realtime_data / volume_30doptional | object | With ?include=volume: a 30-day gross + count, only when the merchant enables realtime data for the partner (else realtime_data:false). |
Channel guardrails
A partner’s supported feature set is enforced per merchant. If a merchant’s program disables a capability, the API returns 403 not_permitted_by_channel even with a valid key + scope:
| Field | Type | Description |
|---|---|---|
Refunds / voidsoptional | guardrail | Blocked when the merchant’s channel disables refunds (a void that only releases an authorization hold is still allowed). |
Reportsoptional | guardrail | A platform key reads a merchant’s reporting only when that merchant enables realtime data for the partner (default off). |
Partner webhooks
Register one webhook endpoint with a platform key (no M360-On-Behalf-Of) and it receives events for all your connected merchants. Each delivery’s payload carries a top-level merchant_id so you know which merchant it’s for. Registration, signing, and retries are otherwise identical to merchant webhooks.
curl -X POST https://www.merchant360.net/api/m360/v1/webhooks \
-H "Authorization: Bearer m360_isv_xxx" \
-d '{ "url": "https://yourapp.com/hooks/m360", "events": ["payment.succeeded"] }'curl -X POST https://www.merchant360.net/api/m360/v1/payments \
-H "Authorization: Bearer m360_isv_xxx" \
-H "M360-On-Behalf-Of: MERCHANT_ID" \
-H "Content-Type: application/json" \
-d '{
"amount": 2500,
"card_token": "card_…"
}'