Payments · ACH · Billing gateway · REST

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.

Overview

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/v1

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

New here? Get a free test key.

Build against the payments, vault, and hosted-fields APIs in test mode – no merchant account needed.

Open the developer sandbox →
Bearer key

Authentication

Create a key in Merchant360 under Developers & API (owner only). Send it as a bearer token on every request:

Authorization: Bearer m360_live_xxx

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

↑ Request – what you send
curl -X GET https://www.merchant360.net/api/m360/v1/me \
  -H "Authorization: Bearer m360_live_xxx"
Per-key permissions

Scopes

Grant a key only what it needs. :read permits GET, :write permits create/update/delete.

FieldTypeDescription
payments:read / :writeoptionalscopeCharge cards, refund, read transactions.
cards:read / :writeoptionalscopeCard vault (tokens) + hosted-field sessions.
checkout:read / :writeoptionalscopeHosted Checkout Sessions.
ach:writeoptionalscopeACH debits and credits.
banks:read / :writeoptionalscopeSaved customer bank accounts (the ACH vault).
payment_links:read / :writeoptionalscopeHosted payment links.
customers:read / :writeoptionalscopeCustomer CRM.
invoices:read / :writeoptionalscopeInvoices (create, send, void).
estimates:read / :writeoptionalscopeEstimates.
credit_memos:read / :writeoptionalscopeCredit memos.
inventory:read / :writeoptionalscopeCatalog products + stock.
tax_rates:read / :writeoptionalscopeTax table.
recurring:read / :writeoptionalscopeSubscriptions.
scheduling:read / :writeoptionalscopeAppointments, service types, classes + class bookings.
reports:readoptionalscopeReport summaries.
bnpl:read / :writeoptionalscopeBuy Now, Pay Later (Affirm + Klarna): sessions, charges, refunds.
vendor_payments:read / :writeoptionalscopeAccounts payable: payees, recorded payments, 1099/W-9.
settings:writeoptionalscopeUpdate accepted methods + installments (read is open on any key).
statements:readoptionalscopeDownload the merchant's processing statement PDF by period.
webhooks:manageoptionalscopeRegister/list/delete webhook endpoints.
Sandbox

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 numberOutcomeCode
4242 4242 4242 4242Approved (AVS: address + ZIP match)00 / Y
4000 0000 0000 0010Approved – AVS: address matches, ZIP does not00 / A
4000 0000 0000 0028Approved – AVS: ZIP matches, address does not00 / Z
4000 0000 0000 0036Approved – AVS: no match00 / N
4000 0000 0000 0002Declined – do not honor05
4000 0000 0000 9995Declined – insufficient funds51
4000 0000 0000 0069Declined – expired card54
4000 0000 0000 0127Declined – incorrect CVV82

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

Envelope

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 (400bad_request, 401unauthorized, 402payment_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.

FieldTypeDescription
400 bad_requestoptionalstatusMalformed or missing parameters.
401 unauthorizedoptionalstatusMissing/invalid key, or not a Merchant360 key.
402 payment_declinedoptionalstatusThe card or refund was declined.
403 insufficient_scopeoptionalstatusThe key lacks the required scope.
404 not_foundoptionalstatusNo such object for this merchant.
409 conflictoptionalstatusIdempotency-Key in progress, or a booking clash.
422 unprocessableoptionalstatusValid shape but rejected by policy (limits, state).
429 rate_limitedoptionalstatusPer-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.

↑ Request – what you send
{
  "error": {
    "message": "Your card was declined.",
    "code": "payment_declined"
  }
}
Safe retries

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-1b2c3d4e5f60
Lists

Pagination

Query
FieldTypeDescription
pageoptionalinteger1-based page number. Default 1.
page_sizeoptionalintegerItems per page, 1–100. Default 25.
↑ Request – what you send
{ "data": [ … ], "page": 1, "page_size": 25, "total": 132, "has_more": true }
POST/GET /payments · payments:*

Payments

POST/paymentsCharge a card. Returns 201, or 402 on decline.
GET/paymentsList recent payments.
POST/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.
POST/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:

origin values
FieldTypeDescription
EMBED_VToptionalstringKeyed on YOUR embedded terminal (embed_key_id names the embed key that carried the session).
EMBED_HPoptionalstringPushed to a physical Handpoint device from your embedded terminal.
M360_VToptionalstringKeyed on the in-app Merchant360 virtual terminal.
M360_HPoptionalstringPushed to a Handpoint device from the in-app hardware terminal.
ISO_VToptionalstringKeyed 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.

Body
FieldTypeDescription
amountrequiredintegerAmount to charge, in cents (> 0).
currencyoptionalstringOnly usd (default).
card_tokenoptionalstringCharge a saved card on file. Use INSTEAD of card. From save_card or POST /tokens.
card_present.entry_modeoptionalstringCard-present read type: chip, contactless, emv_fallback, swipe, or track1. Preview.
card_present.emv_dataoptionalstringDE 55 ICC TLV payload (hex) from the EMV kernel. Required for chip / contactless / emv_fallback.
card_present.track_2optionalstringTrack-2 data (raw swipe, or the chip's track-2 image on an EMV read).
card_present.track_1optionalstringTrack-1 data (track1 entry mode).
card_present.ksnoptionalstringDUKPT Key Serial Number when the capture device encrypts (P2PE).
card.numberoptionalstringPAN, digits (spaces ok). Required unless card_token or card_present is given.
card.exp_monthoptionalstringTwo digits, 0112. Required with card.
card.exp_yearoptionalstringTwo or four digits (27 or 2027). Required with card.
card.cvvoptionalstringSecurity code.
cardholder.nameoptionalstringFull name; split into first/last.
cardholder.emailoptionalstringReceipt/AVS.
cardholder.phoneoptionalstringContact.
cardholder.zipoptionalstringBilling ZIP (AVS).
cardholder.addressoptionalstringBilling street (AVS).
sales_taxrequired to qualifyintegerTax portion in cents. Activates Level II – see Level II & III.
invoice_numberoptionalstringYour 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_codeoptionalstringYour 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 qualifyarrayLevel III line-item detail, max 15 items. Full field reference, tier requirements, and a complete example: Level II & III.
freightoptionalintegerTotal freight / shipping cents included in amount (Level III header field).
descriptionoptionalstringReference shown on the transaction (≤120 chars). Also used as the Level II/III merchant order reference.
save_cardoptionalbooleanVault the keyed card and return a reusable card_token for future charges/subscriptions.
surchargeoptionalstringKeyed 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).
Response
FieldTypeDescription
idoptionalstringPayment id. Use it for refunds, voids, and lookups.
statusoptionalstringsucceeded on approval. A decline returns HTTP 402 with an error envelope, not this object.
amountoptionalintegerTotal charged in cents. With surcharge this is the total; amount_base and amount_surcharge ride alongside.
card.last4optionalstringLast four of the PAN used.
invoice_numberoptionalstringEcho of what you sent (or of the legacy description field). Null when neither was supplied.
customer_codeoptionalstringEcho of what you sent. Null when not supplied.
processor.auth_codeoptionalstringIssuer approval code. This is the number to print on a receipt and to quote when disputing.
processor.response_codeoptionalstringRaw network response code (00 approved, 10 partial approval).
processor.response_messageoptionalstringHuman-readable result from the host. Log it; don't parse it.
processor.avs_resultoptionalstringAddress-verification verdict. Common values: Y address and ZIP match, Z ZIP only, A address only, N neither, U unavailable.
processor.cvv_resultoptionalstringSecurity-code verdict: M match, N no match, P not processed, S should be present but was not, U issuer unavailable.
processor.rrnoptionalstringRetrieval reference number. The strongest key for locating this transaction with the processor later.
card_tokenoptionalstringSaved-card token when save_card or card_token was used. vault_token is a back-compat alias.
metadataoptionalobjectYour key/value metadata, echoed back.
modeoptionalstringlive 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.

GET /payments – query
FieldTypeDescription
page / page_sizeoptionalintegerPagination.
viaoptionalstringFilter to payments whose metadata via equals this value – e.g. the Payment Extension lists only its own charges with via=payment-extension.
GET /payments/surcharge-quote – query
FieldTypeDescription
amountrequiredintegerBase cents owed.
binrequiredstringThe 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.
↑ Request – what you send
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
}'
Interchange qualification · commercial cards

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.

The three data levels – what you send on POST /payments
FieldTypeDescription
Level Ioptionaldata levelA plain charge – amount + card. No additional data submitted.
Level IIoptionaldata levelAdd 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 IIIoptionaldata levelAdd 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_taxSales 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
freightFreight amount + credit/debit indicator, plus discount and duty header fields with their indicators
descriptionMerchant order / customer reference number
cardholder.zip / merchant profileDestination + ship-from postal codes, destination country, order date, entry coding
line_items[] – each item
FieldTypeDescription
descriptionrequiredstring ≤26What was sold. 26 characters is the card-network line-description limit.
unit_costrequiredintegerCents per unit.
commodity_coderequired to qualifystring ≤15NIGP 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.
quantityrecommendedinteger ≥1Whole units. Defaults to 1 when omitted.
amountauto-derivedintegerExtended line total in cents. Defaults to quantity × unit_cost − discount – send it only when your own math differs (rounding).
product_codeauto-derivedstring ≤12SKU / product code. Derived from the description when omitted.
unit_of_measureauto-derivedstring ≤12Unit code – EA each (default), BX box, HR hour, LB pound…
discountoptionalintegerLine 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).
Level II – POST /payments ($134.00 sale incl. $11.56 tax)
↑ Request – what you send
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"
  }
}'
Level III – POST /payments ($89.99 item + $5.95 freight + $7.76 tax = $103.70)
↑ Request – what you send
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
    }
  ]
}'
Level III, multi-item – POST /payments ($84.00 lines + $4.99 freight + $7.25 tax = $96.24)
↑ Request – what you send
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
    }
  ]
}'
Fuel · hotel · auto rental · airline · and more

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"), unlike amount which 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-DD and times are HH: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 industry entirely.
  • Keyed charges only – with or without surcharge: "auto". A card_token charge 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.

Industries carried
FieldTypeDescription
"type": "FUEL"optionalJ26 = 8FuelPump and product detail for a fuel purchase. Mastercard and Discover carry different subsets; Visa fuel rides the Fleet profile. 14 fields.
"type": "AIRLINE"optionalJ26 = 1AirlineTicket, passenger and itinerary detail. Captures the ticket header and the first air segment. 29 fields.
"type": "HEALTHCARE"optionalJ26 = 18HealthcareProvider and payer identifiers. Per-line healthcare eligibility is set on the Level III grid. 3 fields.
"type": "CRUISE"optionalJ26 = 19CruiseSailing, itinerary and the air leg to the port, plus agency identifiers. 19 fields.
"type": "RAIL"optionalJ26 = 15RailTicket, journey and service detail for rail travel. 19 fields.
"type": "ELECTRIC_FUEL"optionalJ26 = 26Electric vehicle chargingEV 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"optionalJ26 = 17Travel agencyAgency identifiers and the service fee charged on a travel booking. 7 fields.
"type": "INSURANCE"optionalJ26 = 22InsurancePolicy, insured party and premium detail. 7 fields.
"type": "TELEPHONE"optionalJ26 = 14TelephoneOriginating and destination numbers for a call-based charge. 3 fields.
"type": "TICKET_ENTERTAINMENT"optionalJ26 = 16Ticketing / entertainmentEvent, venue and ticket detail. 9 fields.
"type": "VISA_TRANSPORT_ANCILLARY"optionalJ26 = 23Transport ancillaryBaggage, seating or other purchases attached to a travel document rather than the ticket. 4 fields.
"type": "HOTEL"optionalJ26 = 4Hotel / lodgingLodging enhanced data - the stay, the room, and itemised incidentals. Carried on all brands. 27 fields.
"type": "AUTO_RENTAL"optionalJ26 = 6Auto rentalVehicle rental enhanced data - agreement, pickup and return detail, distance and adjustments. 21 fields.
"type": "FLEET"optionalJ26 = 25FleetVisa 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.

Site
FieldTypeDescription
company_brand_namerecommendedB62 · an4Brand — Brand at the pump, 4 characters (e.g. SHEL). Pre-filled from the merchant name.
purchase_timerecommendedB63 · n4 · HHMMPurchase time — Local time at the pump, HH:MM. MC only.
fuel_service_typerecommendedB64 · an1 · enumService type Values: S, F, H. MC only.
Product
FieldTypeDescription
fuel_coderequired to qualifyB69 · an2 · enumFuel code — Visa Fuel Type Code. 121 defined values. DISC only.
fuel_unit_pricerequired to qualifyB71 · n12 · 4dp impliedPrice per gallon — Dollars per gallon, e.g. 3.499. MC only.
fuel_quantityrequired to qualifyB72 · n6 · 3dp impliedQuantity (gallons) — Gallons dispensed, e.g. 12.153. MC only.
fuel_sale_amountrecommendedB73 · n12 · 2dp impliedFuel sale amount — The fuel portion of the sale. MC only.
Tax
FieldTypeDescription
total_tax_amountrecommendedB65 · n12 · 2dp impliedTotal tax
total_tax_collect_indicatorrecommendedB66 · an1 · enumTax collected Values: Y, N. MC only.
state_sales_tax_amountrecommendedB67 · n12 · 2dp impliedState sales tax — Pre-filled from the merchant's default tax rate in Settings. DISC only.
state_sales_tax_idrequired to qualifyB68 · an1State tax ID — One character. DISC only.
tax_exempt_numberrequired to qualifyB70 · n12Tax exempt number — Digits only, up to 12. DISC only.
Vehicle
FieldTypeDescription
odometer_readingrequired to qualifyH165 · n7Odometer — Whole miles. Digits only - no commas, no decimals.
Tax
FieldTypeDescription
exempt_indicatorrequired to qualify— · an1Exempt indicator
AirlineJ26 = 129 fields · All brands

Ticket, passenger and itinerary detail. Captures the ticket header and the first air segment.

Ticket
FieldTypeDescription
ticket_numberrequired to qualifyC02 · an15Ticket number
passenger_namerequired to qualifyC10 · an25Passenger name
transaction_typerequired to qualifyC01 · an2Transaction type
document_typerequired to qualifyC03 · an2Document type
ticket_issue_daterequired to qualifyC08 · n8 · YYYYMMDDIssue date
ticket_issue_cityrequired to qualifyC07 · an18Issue city
ticketing_carrierrequired to qualifyC06 · an25Ticketing carrier
iata_coderequired to qualifyC05 · n8IATA code
electronic_ticketrecommendedC14 · an1 · enumElectronic ticket Values: E, P.
restricted_ticketrequired to qualifyC75 · an1 · enumRestricted ticket Values: N, R.
number_in_partyrecommendedC09 · n3Passengers
total_farerecommendedC78 · n12 · 2dp impliedTotal fare
Itinerary
FieldTypeDescription
total_segmentsrecommendedC15 · n2Air segments — Total legs on the ticket. Only the first is captured here.
departure_locationrequired to qualifyC18 · an5From (airport)
arrival_locationrequired to qualifyC20 · an5To (airport)
departure_daterequired to qualifyC19 · n8 · YYYYMMDDDeparture date
departure_timerequired to qualifyC79 · n4 · HHMMDeparture time — HH:MM
arrival_timerequired to qualifyC80 · n4 · HHMMArrival time — HH:MM
segment_carrierrequired to qualifyC21 · an4Carrier
flight_numberrequired to qualifyC24 · an6Flight number
class_of_servicerequired to qualifyC23 · an3Class of service
fare_basisrequired to qualifyC22 · an15Fare basis
segment_farerequired to qualifyC25 · n12 · 2dp impliedSegment fare
stop_overrequired to qualifyC17 · an1 · enumStopover Values: O, X.
Agency
FieldTypeDescription
travel_agency_coderequired to qualifyC73 · an8Agency code
travel_agency_namerequired to qualifyC74 · an25Agency name
customer_coderequired to qualifyC71 · an17Customer code
ticket_change_indicatorrequired to qualifyC77 · an1Ticket change
credit_reason_indicatorrequired to qualifyC76 · an1Credit reason
HealthcareJ26 = 183 fields · All brands

Provider and payer identifiers. Per-line healthcare eligibility is set on the Level III grid.

Provider
FieldTypeDescription
provider_idrequired to qualifyP27 · an15Provider ID — Visa healthcare provider identifier. VISA only.
service_type_coderequired to qualifyP28 · an4Service type VISA only.
payer_idrequired to qualifyP31 · an15Payer ID
CruiseJ26 = 1919 fields · All brands

Sailing, itinerary and the air leg to the port, plus agency identifiers.

Booking
FieldTypeDescription
passenger_namerequired to qualifyC38 · an25Passenger name
ticket_numberrequired to qualifyC39 · an15Ticket number
cruise_namerequired to qualifyC50 · an25Cruise / ship name
departure_daterequired to qualifyC46 · n8 · YYYYMMDDDeparture date
return_daterequired to qualifyC47 · n8 · YYYYMMDDReturn date
number_of_daysrequired to qualifyC49 · n3Nights
total_costrecommendedC48 · n12 · 2dp impliedTotal cost
class_coderequired to qualifyC45 · an3Class
travel_packagerequired to qualifyC37 · an1 · enumTravel package Values: Y, N.
Itinerary
FieldTypeDescription
destination_coderequired to qualifyC41 · an5Destination
city_namerequired to qualifyC53 · an18City
region_coderequired to qualifyC51 · an3Region
country_coderequired to qualifyC52 · an3Country
Air
FieldTypeDescription
departure_airportrequired to qualifyC42 · an5Departure airport
air_carrier_coderequired to qualifyC43 · an4Air carrier
flight_numberrequired to qualifyC44 · an6Flight number
depart_daterequired to qualifyC40 · n8 · YYYYMMDDFlight date
Agency
FieldTypeDescription
iata_carrier_coderequired to qualifyC35 · an4IATA carrier
iata_agency_numberrequired to qualifyC36 · an8IATA agency number
RailJ26 = 1519 fields · All brands

Ticket, journey and service detail for rail travel.

Ticket
FieldTypeDescription
transaction_typerequired to qualifyC26 · an2Transaction type
ticket_numberrequired to qualifyC27 · an15Ticket number
passenger_namerequired to qualifyC28 · an25Passenger name
carrier_coderequired to qualifyC29 · an4Carrier
issuer_namerequired to qualifyC30 · an25Issuer name
issuer_cityrequired to qualifyC31 · an18Issuer city
Journey
FieldTypeDescription
departure_locationrequired to qualifyC32 · an5From
arrival_locationrequired to qualifyC34 · an5To
departure_daterequired to qualifyC33 · n8 · YYYYMMDDDeparture date
rail_classrequired to qualifyC60 · an3Class
number_of_adultsrecommendedC58 · n3Adults
number_of_childrenrequired to qualifyC59 · n3Children
Service
FieldTypeDescription
traveller_namerequired to qualifyC54 · an25Traveller name
service_ticket_numrequired to qualifyC55 · an15Service ticket number
service_typerequired to qualifyC56 · an3Service type
service_naturerequired to qualifyC57 · an3Service nature
service_amountrequired to qualifyC61 · n12 · 2dp impliedService amount
service_amount_signrequired to qualifyC62 · an1 · enumAmount sign Values: D, C.
procedure_idrequired to qualifyC63 · an8Procedure 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.

Session
FieldTypeDescription
connector_typerequired to qualifyS30 · an3 · enumConnector type 9 defined values.
unit_of_measurerecommendedS37 · an1 · enumUnit of measure — Electric sessions bill by kWh or by minute. Values: W, C.
quantityrequired to qualifyS42 · n12 · 4dp impliedQuantity (kWh)
unit_pricerequired to qualifyS39 · n12 · 4dp impliedPrice per kWh
total_including_taxrecommendedS48 · n12 · 2dp impliedTotal including tax
start_timerequired to qualifyS46 · n4 · HHMMCharge start — HH:MM
finish_timerequired to qualifyS47 · n4 · HHMMCharge finish — HH:MM
total_charging_timerequired to qualifyS45 · n6Charging time (min)
total_time_plugged_inrequired to qualifyS44 · n6Plugged in (min) — Can exceed charging time - idle minutes are often billed separately.
Station
FieldTypeDescription
max_power_dispensedrequired to qualifyS31 · n6Max power dispensed (kW)
power_capacityrequired to qualifyS36 · n6Station capacity (kW) — May exceed max dispensed when the site manages power.
charging_reason_coderequired to qualifyS35 · an3 · enumCharging reason — Only when the session ended abnormally. 10 defined values.
Vehicle
FieldTypeDescription
est_miles_addedrequired to qualifyS34 · n6Est. miles added
est_vehicle_miles_availablerequired to qualifyS32 · n6Est. range on leaving
carbon_footprintrequired to qualifyS33 · n12Carbon avoided (g CO2e)
Travel agencyJ26 = 177 fields · All brands

Agency identifiers and the service fee charged on a travel booking.

Agency
FieldTypeDescription
agency_coderequired to qualifyH101 · an8Agency code
agency_namerecommendedH102 · an25Agency name
agency_seq_numberrequired to qualifyH085 · an8Sequence number
Fee
FieldTypeDescription
fee_amountrequired to qualifyH086 · n12 · 2dp impliedAgency fee
fee_amount_signrecommendedH087 · an1 · enumFee sign Values: D, C.
fee_raterequired to qualifyH088 · n6 · 2dp impliedFee rate (%)
fee_descriptionrecommendedH089 · an25Fee description
InsuranceJ26 = 227 fields · All brands

Policy, insured party and premium detail.

Policy
FieldTypeDescription
policy_numberrequired to qualifyH148 · an25Policy number
additional_policy_numberrequired to qualifyH152 · an25Additional policy number
type_of_policyrequired to qualifyH153 · an25Policy type
name_of_insuredrequired to qualifyH154 · an30Name of insured
Premium
FieldTypeDescription
premium_frequencyrequired to qualifyH151 · an12 · enumPremium frequency Values: Monthly, Quarterly, Annual, Single.
insurance_amountrecommendedH077 · n12 · 2dp impliedPremium amount
insurance_indicatorrecommendedH131 · an1 · enumInsurance indicator Values: Y, N.
TelephoneJ26 = 143 fields · All brands

Originating and destination numbers for a call-based charge.

Call
FieldTypeDescription
call_from_phone_numberrequired to qualifyJ73 · n15 · digits onlyCall from
call_to_phone_numberrequired to qualifyJ77 · n15 · digits onlyCall to
phone_card_idrequired to qualifyJ78 · an20Phone card ID
Ticketing / entertainmentJ26 = 169 fields · All brands

Event, venue and ticket detail.

Event
FieldTypeDescription
event_namerecommendedJ60 · an25Event name
event_daterequired to qualifyJ61 · n8 · YYYYMMDDEvent date
event_locationrequired to qualifyJ64 · an25Venue
event_region_coderequired to qualifyJ65 · an3Region
event_country_coderequired to qualifyJ66 · an3Country
Tickets
FieldTypeDescription
ticket_quantityrecommendedJ63 · n4Tickets
individual_ticket_pricerequired to qualifyJ62 · n12 · 2dp impliedPrice per ticket
ticket_typerequired to qualifyC86 · an4Ticket type
issuer_addressrequired to qualifyC83 · an25Issuer address
Transport ancillaryJ26 = 234 fields · All brands

Baggage, seating or other purchases attached to a travel document rather than the ticket.

Document
FieldTypeDescription
ticket_document_numberrequired to qualifyC65 · an15Ticket document number
additional_document_numberrequired to qualifyC69 · an15Additional document number
passenger_namerequired to qualifyC68 · an25Passenger name
credit_reason_indicatorrequired to qualifyC70 · an1Credit reason
Hotel / lodgingJ26 = 427 fields · All brands

Lodging enhanced data - the stay, the room, and itemised incidentals. Carried on all brands.

Stay
FieldTypeDescription
arrival_daterequired to qualifyH016 · n8 · YYYYMMDDArrival date
departure_daterequired to qualifyH017 · n8 · YYYYMMDDDeparture date
folio_numberrequired to qualifyH018 · an12Folio number
room_raterequired to qualifyH021 · n12 · 2dp impliedRoom rate (nightly)
room_taxrequired to qualifyH022 · n12 · 2dp impliedRoom tax
number_of_roomsrecommendedH008 · n3Rooms booked
number_of_adultsrequired to qualifyH009 · n3Adults
no_show_indicatorrecommendedH011 · an1 · enumNo-show Values: N, Y.
Room
FieldTypeDescription
room_typerequired to qualifyH006 · an12Room type
bed_typerequired to qualifyH005 · an12Bed type
room_locationrequired to qualifyH004 · an12Room location
smoking_preferencerequired to qualifyH007 · an1 · enumSmoking Values: N, S.
rate_typerequired to qualifyH012 · an12Rate type
program_coderequired to qualifyH023 · an12Program code
promotional_coderequired to qualifyH001 · an12Promotional code
corporate_client_coderequired to qualifyH003 · an12Corporate client code
Incidentals
FieldTypeDescription
phone_chargesrequired to qualifyH024 · n12 · 2dp impliedPhone
restaurant_chargesrequired to qualifyH025 · n12 · 2dp impliedRestaurant / room service
mini_bar_chargesrequired to qualifyH026 · n12 · 2dp impliedMini bar
laundry_chargesrequired to qualifyH027 · n12 · 2dp impliedLaundry
gift_shop_chargesrequired to qualifyH030 · n12 · 2dp impliedGift shop
movie_chargesrequired to qualifyH032 · n12 · 2dp impliedMovies
health_club_chargesrequired to qualifyH033 · n12 · 2dp impliedHealth club
valet_parking_chargesrequired to qualifyH034 · n12 · 2dp impliedValet parking
cash_disbursement_chargesrequired to qualifyH035 · n12 · 2dp impliedCash disbursement
other_chargesrequired to qualifyH028 · n12 · 2dp impliedOther
adjustment_amountrequired to qualifyH020 · n12 · 2dp impliedAdjustment
Auto rentalJ26 = 621 fields · All brands

Vehicle rental enhanced data - agreement, pickup and return detail, distance and adjustments.

Agreement
FieldTypeDescription
agreement_numberrequired to qualifyB01 · an25Agreement number — Rental agreement number signed by the cardholder.
rate_indicatorrequired to qualifyB38 · an1 · enumRate type Values: D, W, M.
raterequired to qualifyB39 · n12 · 2dp impliedRate
vehicle_class_idrequired to qualifyB14 · an4Vehicle class
driver_tax_numberrequired to qualifyB22 · an20Driver tax number
Pickup
FieldTypeDescription
pickup_daterequired to qualifyB06 · n8 · YYYYMMDDPickup date
pickup_timerequired to qualifyB07 · n4 · HHMMPickup time — HH:MM
pickup_locationrequired to qualifyB02 · an26Location
pickup_cityrequired to qualifyB03 · an18City
pickup_regionrequired to qualifyB04 · an3State / region
pickup_countryrequired to qualifyB05 · an3Country
Return
FieldTypeDescription
return_daterequired to qualifyB11 · n8 · YYYYMMDDReturn date
return_timerequired to qualifyB12 · n4 · HHMMReturn time — HH:MM
dropoff_locationrequired to qualifyB19 · an26Drop-off location
return_cityrequired to qualifyB08 · an25City
return_regionrequired to qualifyB09 · an3State / region
return_countryrequired to qualifyB10 · an3Country
distancerequired to qualifyB15 · n5Distance travelled — Whole units.
distance_uomrecommendedB16 · an1 · enumDistance unit Values: M, K.
adjustment_indicatorrequired to qualifyB17 · an1Adjustment type
adjustment_amountrequired to qualifyB18 · n12 · 2dp impliedAdjustment 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.

Purchase
FieldTypeDescription
business_application_idrecommendedP25 · an2Business application — Fleet business application identifier. F1 per Cygma's Visa Fleet sample.
Product
FieldTypeDescription
fleet_fuel_typeoptionalH157 · an2Fuel 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.
Purchase
FieldTypeDescription
type_of_purchaserequired to qualifyS51 · an1 · enumType of purchase — MANDATORY on fleet. Drives which of the fields below Visa requires. Values: 1, 2, 3, 4.
expanded_fuel_typeoptionalS25 · an4 · enumFuel 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_typeoptionalS52 · an1 · enumService 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_measureoptionalS37 · an1 · enumUnit 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.
quantityoptionalS42 · n12 · 4dp impliedQuantity (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_costoptionalS39 · n12 · 4dp impliedPrice 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_priceoptionalS55 · n12 · 4dp impliedGross 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_priceoptionalH162 · n12 · 4dp impliedNet fuel price — Optional. Quantity x cost EXCLUSIVE of taxes. Applies only when type_of_purchase is “1” or “3” or “4”. Fuel only.
Non-fuel
FieldTypeDescription
gross_non_fuel_priceoptionalH163 · n12 · 2dp impliedGross 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_priceoptionalH164 · n12 · 2dp impliedNet non-fuel price — Optional, exclusive of taxes. Applies only when type_of_purchase is “2” or “3”. Non-fuel only.
Vehicle
FieldTypeDescription
odometer_readingrequired to qualifyH165 · n7Odometer — Whole miles. Digits only - no commas, no decimals.
employee_numberrecommendedS26 · an12Employee number — When the card prompts for it. Defaults to 1.
trailer_numberrequired to qualifyS27 · an16Trailer number — When the card prompts for it.
prompted_data1required to qualifyS28 · an20Prompted data 1
prompted_data2required to qualifyS29 · an20Prompted 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.

↑ Request – what you 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"
  }
}
Two-step · payments:write

Authorize & capture

POST/payments/authorizePlace a hold (no money moves).
POST/payments/{id}/incrementRaise the hold – incremental auth (bar tab / hospitality): { "amount": 500, "card": {…} }.
POST/payments/{id}/captureSettle up to the authorized amount (partial ok): { "amount": 4200 }.
POST/payments/{id}/voidRelease the hold (no body).
POST/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.

Authorization lifecycle
FieldTypeDescription
requires_captureoptionalstatusHold placed; amount_capturable shows what you can still capture.
succeededoptionalstatusCaptured (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.

POST /payments/authorize – body
FieldTypeDescription
amountrequiredintegerAmount to authorize, in cents (> 0).
currencyoptionalstringOnly usd (default).
card.numberrequiredstringPAN (keyed). Saved-card pre-auth is a planned follow-up.
card.exp_monthrequiredstringTwo digits, 0112.
card.exp_yearrequiredstringTwo or four digits.
card.cvvoptionalstringSecurity code.
cardholder.*optionalobjectSame shape as POST /payments (name/email/phone/zip/address, for AVS).
descriptionoptionalstringReference on the transaction (≤120 chars).
POST /payments/{id}/capture – body
FieldTypeDescription
amountoptionalintegerCents 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.

POST /payments/{id}/increment – body
FieldTypeDescription
amountrequiredintegerADDITIONAL cents to authorize (> 0).
card / card_tokenrequiredobject / stringThe card again – keyed, or a saved card on file.
cumulative_amountoptionalintegerOverride the computed running total (original + approved increments + this amount).
descriptionoptionalstringReference 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.

POST /payments/{id}/tip-adjust – body
FieldTypeDescription
tip_amountrequiredintegerTip cents being added (> 0).
amountoptionalintegerCorrected 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.

Authorize – POST /payments/authorize ($10.00 hold)
↑ Request – what you send
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"
}'
Increment – POST /payments/{id}/increment (+$5.00 → tab $15.00)
↑ Request – what you send
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"
  }
}'
Capture – POST /payments/{id}/capture (close the $25.00 tab)
↑ Request – what you send
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
}'
Tip adjust – POST /payments/{id}/tip-adjust (+$5.00 tip → $30.00)
↑ Request – what you send
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
}'
Void – POST /payments/{id}/void (release the hold)
↑ Request – what you send
curl -X POST https://www.merchant360.net/api/m360/v1/payments/AUTH_ID/void \
  -H "Authorization: Bearer m360_live_xxx"
POST /payments/{id}/refund · /void · payments:write

Refunds & voids

POST/payments/{id}/refundRefund a settled charge (full or partial; omit amount for full).
POST/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.

↑ Request – what you send
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
}'
/tokens · cards:*

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.

POST /tokens – body (cards:write)
FieldTypeDescription
card.numberrequiredstringPAN to vault, digits (spaces ok).
card.exp_monthrequiredstringTwo digits, 0112.
card.exp_yearrequiredstringTwo or four digits (27 or 2027).
cardholder.nameoptionalstringLabel for the saved card.
cardholder.zipoptionalstringBilling ZIP stored for AVS on future charges.
cardholder.addressoptionalstringBilling street stored for AVS.
set_defaultoptionalbooleanMake this the merchant's default card on file.

Try the live vault demo – save a card, then charge it repeatedly.

POST/tokensTokenize a card without charging it (cards:write).
GET/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.
GET/tokens/{id}Retrieve one saved card (cards:read).
DELETE/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.

↑ Request – what you send
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
}'
Browser tokenization · keep the PAN off your servers

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:

POST/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.
POST/hosted/tokenizeCalled by the iframe (not you) to turn the card into a card_token.
POST /hosted/sessions – body
FieldTypeDescription
expires_inoptionalintegerClient-token lifetime in seconds. Default 900 (15 min); clamped to 60–3600.
merchant360.js
FieldTypeDescription
Merchant360(clientToken)optionalfnCreate an instance from the client token minted in step 1.
.hostedFields({ style })optionalfnCreate the fields. style.accent + style.fontFamily theme them.
.mount(selector)optionalfnInsert the iframe into your page.
.on('change', cb)optionalfnLive field validity → { complete, brand }.
.tokenize()optionalfnPromise → { 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).

↑ Request – what you send
// 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>
POST /ach/debits · /ach/credits · GET /ach/{id} · ach:write

ACH debits & credits

POST/ach/debitsPull from a customer bank account.
POST/ach/creditsPush to a customer bank account (same body).
GET/ach/{id}Retrieve an entry – poll its status, or use the webhooks below.
GET/ach/routing/{number}Routing-number lookup: Mod-10 checksum + FedACH bank name – show “Debiting <bank>” before you submit. Any valid key; no scope.
POST/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.

Lifecycle
FieldTypeDescription
queuedoptionalstatusAccepted, waiting for the next 4:00 PM ET submission window. No money has moved.
processingoptionalstatusSubmitted to the bank; awaiting settlement (typically 1–2 banking days).
settledoptionalstatusFunds moved; settled_at is set. A late bank return can still move a settled entry to returned – see below.
returnedoptionalstatusThe bank returned the entry; return.code / return.description say why.
canceledoptionalstatusCanceled before submission. Terminal.
failedoptionalstatusRejected 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.

GET /ach/{id} – response
FieldTypeDescription
idoptionalstringEntry id (ach_…).
objectoptionalstringach_payment.
kindoptionalstringdebit or credit.
statusoptionalstringqueued · processing · settled · returned · canceled · failed.
amount_centsoptionalintegerAmount in cents.
currencyoptionalstringusd.
sec_codeoptionalstringPPD / WEB / CCD – resolved server-side (above).
run_dateoptionalstringBanking day the entry submits (or submitted) to the bank.
createdoptionalstringISO timestamp.
settled_atoptionalstringISO timestamp once settled; null before.
returnoptionalobject{ code, description } once returned; null otherwise.
resubmitted_asoptionalstringId of the automatic re-presentment when an R01/R09 was retried.
ACH errors
FieldTypeDescription
400 no_authoptionalerrorDebits require a stored customer authorization.
403 not_enabled / not_allowedoptionalerrorACH 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_limitoptionalerrorPer-transaction or monthly volume cap exceeded.
422 not_validatedoptionalerrorThe bank account hasn't completed verification – a prenote takes 1–2 banking days.
502 queue_failedoptionalerrorThe 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.

Body
FieldTypeDescription
amountrequiredintegerCents (> 0).
bank_account_idoptionalstringCharge 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.routingoptionalstring9-digit ABA routing number. Required unless bank_account_id is given.
bank_account.accountoptionalstringAccount number. Required unless bank_account_id is given.
bank_account.holder_nameoptionalstringName on the account. Required unless bank_account_id is given.
bank_account.typeoptionalstringchecking (default) or savings.
bank_account.holder_typeoptionalstringconsumer (PPD/WEB, default) or business (CCD).
authorization.frequencyoptionalstringone_time (default) or recurring.
descriptionoptionalstringNACHA 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.

↑ Request – what you send
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"
}'
/banks · /banks/{id} · banks:*

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.

POST/banksStore a bank on file without charging (banks:write). Returns the bank_… token.
GET/banksList saved banks (banks:read).
GET/banks/{id}Retrieve one saved bank (banks:read).
DELETE/banks/{id}Remove a saved bank – a soft archive (banks:write).
POST /banks – body (banks:write)
FieldTypeDescription
bank_account.routingrequiredstring9-digit ABA routing number.
bank_account.accountrequiredstringAccount number (stored encrypted; only last 4 is returned).
bank_account.holder_namerequiredstringName on the account.
bank_account.typeoptionalstringchecking (default) or savings.
bank_account.holder_typeoptionalstringconsumer (PPD/WEB, default) or business (CCD).
receipt_emailoptionalstringOptional. Where the receipt is emailed when this bank is charged standalone (a debit not tied to an invoice or payment link). Sent on settlement.
Bank object
FieldTypeDescription
idoptionalstringThe bank_… token – pass it as bank_account_id on an ACH debit/credit.
objectoptionalstringAlways bank_account.
holder_name / routing / last4optionalstringAccount holder, public ABA routing, and last 4 (never the full account).
account_type / holder_typeoptionalstringchecking/savings and consumer/business.
sec_codeoptionalstringPPD / CCD derived from holder_type (informational).
validation_statusoptionalstringunverifiedprenote_pendingprenote_verified / verified (or failed). A prenote takes 1–2 banking days.
is_defaultoptionalbooleanThe merchant's default bank for this customer scope.
receipt_emailoptionalstringThe 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.

↑ Request – what you send
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 · customers:*

Customers

POST/customersCreate a customer.
GET/customersList customers (?search=).
GET/customers/{id}Retrieve a customer.
PATCH/customers/{id}Update a customer.
GET/customers/{id}/autopayAutopay status (card on file that auto-pays future invoices).
POST/customers/{id}/autopayEnroll a saved card (card_token + consent:true) so future invoices auto-charge on their due date.
DELETE/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.

Body (create / update)
FieldTypeDescription
namerequiredstringDisplay name (required on create).
legal_nameoptionalstringLegal/business name.
emailoptionalstringEmail.
phoneoptionalstringPhone.
address.line1optionalstringBilling street.
address.city / state / zipoptionalstringBilling city / state / ZIP.
↑ Request – what you send
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:*

Catalog & inventory

POST/catalogCreate a catalog item.
GET/catalogList items (?include_pos=true to include register-only menu items).
GET/catalog/{id}Retrieve an item.
PATCH/catalog/{id}Update an item.
DELETE/catalog/{id}Archive an item.
POST/catalog/{id}/stockRecord a stock movement.
Body (create / update)
FieldTypeDescription
namerequiredstringProduct/service name.
pricerequiredintegerUnit price in cents.
costoptionalintegerUnit cost in cents.
skuoptionalstringStock-keeping unit.
categoryoptionalstringCategory label.
descriptionoptionalstringDescription.
typeoptionalstringproduct (default), service, or digital.
taxableoptionalbooleanWhether tax applies. Default true.
track_stockoptionalbooleanTrack on-hand quantity. Default false.
quantityoptionalintegerOpening on-hand (when tracking stock).
pos_itemoptionalbooleanShow in the TableTurn register. Default false (catalog-only).
POST /catalog/{id}/stock – body
FieldTypeDescription
quantityrequiredintegerNon-zero; positive receives, negative removes.
reasonoptionalstringreceived, returned, adjustment (default), or sold.
notesoptionalstringe.g. a PO number.
↑ Request – what you send
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_rates:*

Tax rates

GET/tax-ratesList tax rates.
POST/tax-ratesCreate a tax rate.
PATCH/tax-rates/{id}Update a tax rate.
DELETE/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%).

Body (create / update)
FieldTypeDescription
namerequiredstringLabel (≤40 chars).
rate_bpsrequiredintegerRate in basis points (625 = 6.25%), 0–5000.
stateoptionalstringTwo-letter state code this rate applies to.
defaultoptionalbooleanMake this the default rate.
↑ Request – what you send
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 · invoices:*

Invoices

POST/invoicesCreate an invoice (add "send": true to email it).
GET/invoicesList invoices (?status=).
GET/invoices/{id}Retrieve an invoice.
POST/invoices/{id}/payCharge a card (card_token or keyed card) against the invoice; flips it to partial/paid.
POST/invoices/{id}/sendEmail the invoice to the customer.
POST/invoices/{id}/textText (SMS) the pay link to the customer – STOP-compliant.
POST/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.

Lifecycle
FieldTypeDescription
DRAFToptionalstatusCreated but not sent; freely editable.
OPENoptionalstatusSent to the customer; awaiting payment on the hosted page.
PARTIALoptionalstatusPartially paid; paid shows cents collected so far.
PAIDoptionalstatusPaid in full. Fires invoice.paid.
VOIDoptionalstatusVoided. 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.

Body
FieldTypeDescription
customer.namerequiredstringBill-to name.
customer.email / phoneoptionalstringContact (email needed to send).
customer.address1 / city / state / zipoptionalstringBill-to address.
line_items[]requiredarrayOne or more line items (below).
line_items[].descriptionrequiredstringLine description.
line_items[].quantityoptionalnumberQuantity. Default 1.
line_items[].unit_pricerequiredintegerUnit price in cents.
tax_rate_bpsoptionalintegerInvoice-level tax in basis points.
due_dateoptionalstringDue date, YYYY-MM-DD.
invoice_dateoptionalstringIssue date, YYYY-MM-DD. Default today.
notesoptionalstringCustomer-facing note.
sendoptionalbooleanEmail the invoice on create.
↑ Request – what you send
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 · estimates:*

Estimates

POST/estimatesCreate an estimate (identical body to invoices).
GET/estimatesList estimates.
POST/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.

↑ Request – what you send
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 · credit_memos:*

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.

POST/credit-memosCreate a credit memo.
GET/credit-memosList credit memos.
POST/credit-memos/{id}/applyApply the memo to an invoice.
POST/credit-memos/{id}/voidVoid (reverse) the memo.
Body
FieldTypeDescription
invoice_idrequiredstringThe invoice to credit.
amountrequiredintegerCredit amount in cents (> 0).
kindoptionalstringwriteoff, adjustment (default), or chargeback.
reasonoptionalstringInternal reason (≤500 chars).
applyoptionalbooleanApply immediately (vs. leave as a draft).
↑ Request – what you send
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
}'
Embed · pay-widget.js · POST /api/cygma/v1/charge

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.

Embed attributes
FieldTypeDescription
data-merchantrequiredstringYour merchant UUID (shown on the Pay Widget page).
data-moderequiredstringOne of the modes below (must be enabled for your account).
data-amountoptionalintegerCents – FIXED_AMOUNT mode only.
data-descriptionoptionalstringShown on the form and the receipt.
Modes
FieldTypeDescription
FIXED_AMOUNToptionalmodeYou set the amount; the customer just pays.
DONATIONoptionalmodePreset amount chips (data-suggested, whole dollars) + custom amount within your min/max.
EXTERNAL_INVOICEoptionalmodeCustomer types your invoice number + amount (pre-fill with ?invoice= and ?memo=).
M360_INVOICEoptionalmodeLook-up-my-invoice: customer finds their open Merchant360 invoice by number + email and pays the balance.
SUBSCRIPTIONoptionalmodeCustomer joins a membership program you define (first charge now, card vaulted, auto-bills on schedule).
SAVE_CARDoptionalmodeVault 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).

POST /api/cygma/v1/charge – body
FieldTypeDescription
moderequiredstringFIXED_AMOUNT | DONATION | EXTERNAL_INVOICE.
amountCentsrequiredinteger100 – 100,000,000 (must fit your configured min/max).
customerrequiredobject{ name, email, phone? } – the receipt goes to this email.
cardrequiredobject{ number, expMonth, expYear, cvv, postalCode, street1?, city?, state? }.
descriptionoptionalstringShown on the receipt.
externalInvoiceRefoptionalstringEXTERNAL_INVOICE – your invoice number (search-indexed in M360).
externalInvoiceMemooptionalstringEXTERNAL_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.

↑ Request – what you send
<!-- 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 · checkout:*

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.

POST/checkout/sessionsCreate a session; returns the hosted url to redirect to.
GET/checkout/sessionsList sessions.
GET/checkout/sessions/{id}Retrieve one – poll for status/payment (or use the webhook).
Body
FieldTypeDescription
amountoptionalintegerTotal to collect, in cents. Required unless line_items is given.
line_itemsoptionalobject[][{ name, amount (cents), quantity }]. Their sum is the total.
success_urlrequiredstringRedirect after payment. {CHECKOUT_SESSION_ID} is substituted; otherwise session_id is appended.
cancel_urloptionalstringWhere the customer returns if they cancel.
descriptionoptionalstringShown on the hosted page (≤300 chars).
customer.name / emailoptionalstringPre-fill the payer's details.
customer.phoneoptionalstringThe 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_idoptionalstringYour own id, echoed back on the session + webhook.
metadataoptionalobjectArbitrary key/values echoed back.
expires_in_minutesoptionalintegerAuto-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 callbackonComplete 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.

↑ Request – what you send
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"
}'
/terminal · terminal:*

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.

GET/terminal/devicesList paired readers. available:false = hide the option.
POST/terminal/chargesPush a sale to a reader (async – returns pending).
GET/terminal/charges/{id}Poll the outcome: pending → approved / declined / cancelled. `raw_status` echoes Handpoint's own outcome word for diagnostics.
POST/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.

↑ Request – what you send
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:*

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.

Statuses
FieldTypeDescription
activeoptionalstatusBilling normally (includes trials – first charge deferred to trial end).
past_dueoptionalstatusA charge declined; the dunning retry ladder is working the balance.
pausedoptionalstatusNo charges until resumed – set manually or by ladder exhaustion.
cancelledoptionalstatusStopped – manually or by ladder exhaustion. Fires subscription.canceled.
endedoptionalstatusNatural completion: occurrence count, end date, or amount cap reached.
failedoptionalstatusBilling 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.

Core
FieldTypeDescription
plan_namerequiredstringName of the plan/subscription.
intervalrequiredstringBilling cadence: weekly, biweekly, monthly, quarterly, semiannual, annual, or custom.
interval_daysoptionalintegerDays between charges when interval is custom.
fire_dayoptionalintegerFor weekly, day of week to charge (0=Sun … 6=Sat).
amountoptionalintegerPer-cycle amount in cents. Required for flat pricing; ignored for usage-based.
start_atoptionalstringFirst charge date, YYYY-MM-DD. Default today.
customer_idoptionalstringAttach to a customer.
Payment method
FieldTypeDescription
payment_methodoptionalstringcard (default) or ach.
vault_tokenoptionalstringRequired for card plans – obtain from a payment with save_card: true.
Trial, setup fee & discount
FieldTypeDescription
trial_daysoptionalintegerFree-trial length; the first recurring charge is deferred by this many days.
setup_feeoptionalintegerOne-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_offoptionalnumberPercent off every cycle (e.g. 10 = 10%).
discount.amount_offoptionalintegerFlat amount off every cycle, in cents. Use one of percent_off / amount_off.
Usage-based pricing (pricing.*)
FieldTypeDescription
pricing.modeoptionalstringflat (default, uses amount), per_unit, or tiered.
pricing.unit_priceoptionalintegerCents per unit (per_unit mode).
pricing.unit_labeloptionalstringUnit name, e.g. seat.
pricing.tiers[]optionalarrayTiered pricing (tiered mode): [{ up_to, unit_price }]. Use up_to: null for the final open tier.
End condition (ends.*)
FieldTypeDescription
ends.afteroptionalintegerStop after N charges.
ends.onoptionalstringStop on a date, YYYY-MM-DD.
ends.at_amountoptionalintegerStop once this total (cents) has been billed.

Manage

GET/subscriptionsList subscriptions.
GET/subscriptions/{id}Retrieve a subscription.
PATCH/subscriptions/{id}Update status: { "status": "pause" } – accepts active, pause, cancel.
POST/subscriptions/{id}/chargeCharge off-cycle now.
↑ Request – what you send
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 · scheduling:*

Appointments

POST/appointmentsBook an appointment.
GET/appointmentsList appointments (?from= & ?to=).
GET/appointments/{id}Retrieve an appointment.
PATCH/appointments/{id}Reschedule / update.
GET/service-typesList bookable service types (durations, prices, colors, buffers).
GET/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 – scheduledconfirmedin_progresscompleted, 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.

Body
FieldTypeDescription
titlerequiredstringWhat the appointment is.
start_atrequiredstringISO datetime.
end_atrequiredstringISO datetime, after start.
service_type_idoptionalstringA service from GET /service-types.
customer_idoptionalstringAttach to an existing customer.
customer_name / email / phoneoptionalstringInline customer details.
assigned_user_idoptionalstringStaff member assigned.
assignee_nameoptionalstringFree-text assignee name.
locationoptionalstringLocation/address text.
priceoptionalintegerPrice in cents.
notesoptionalstringInternal notes.
forceoptionalbooleanBook even if the assignee is already busy (else 409).
PATCH /appointments/{id} – body
FieldTypeDescription
statusrequiredstringscheduled, confirmed, in_progress, completed, canceled, no_show.
↑ Request – what you send
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 · scheduling:*

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.

GET/classesList classes (?active= filter).
POST/classesCreate a class.
GET/classes/{id}Retrieve a class.
PATCH/classes/{id}Update (partial). Deactivate with active: false – classes with booking history are never deleted.
GET/class-sessionsList sessions with spots_left (?class_id=, ?from=, ?to=).
POST/class-sessionsSchedule sessions – repeat_weeks creates a weekly series.
GET/class-sessions/{id}Retrieve a session.
PATCH/class-sessions/{id}Cancel / reinstate (status: canceled | scheduled).
GET/class-sessions/{id}/bookingsSession roster (all statuses).
POST/class-sessions/{id}/bookingsAdd an attendee (walk-in / phone booking).
GET/class-bookings/{id}Retrieve a booking.
DELETE/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.

POST /classes – body
FieldTypeDescription
titlerequiredstringClass name (≤120 chars).
descriptionoptionalstringShown in the public booking modal.
instructor_staff_idoptionalstringA staff id from GET /staff.
capacityoptionalintegerMax people per session. Default 10.
payment_modeoptionalstringfree (RSVP only), full (pay price to book), deposit (pay deposit to book). Default free.
priceoptionalintegerCents. Required > 0 for full / deposit.
depositoptionalintegerCents due at booking (deposit mode); can't exceed price.
coloroptionalstring#rrggbb calendar color.
require_email / require_phoneoptionalbooleanPublic bookings must include an email / mobile number.
activeoptionalbooleanBookable on the public calendar. Default true.
POST /class-sessions – body
FieldTypeDescription
class_idrequiredstringThe class to schedule.
start_atrequiredstringISO datetime.
duration_minoptionalintegerDefault 60.
capacity_overrideoptionalintegerOverride the class capacity for these sessions.
repeat_weeksoptionalinteger1–52; >1 creates a weekly series sharing a series_id.
POST /class-sessions/{id}/bookings – body
FieldTypeDescription
namerequiredstringAttendee name.
emailoptionalstringAttendee email (used for waitlist promotion emails).
phoneoptionalstringAttendee mobile number.
marketing_opt_inoptionalbooleanAdds them to the class marketing list export.
waitlistoptionalbooleanJoin the waitlist instead of failing with 409 sold_out when full.
↑ Request – what you send
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"
}'
/shifts · scheduling:*

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.

GET/shiftsList shifts in a window (?from=, ?to=, ISO; default this week + next). Includes the roster of worker keys.
POST/shiftsCreate a draft shift.
GET/shifts/{id}Retrieve a shift.
PATCH/shifts/{id}Update (partial): times, break_min, role, note, or reassign via worker_key.
DELETE/shifts/{id}Remove a shift.
POST/shifts/publishPublish every draft in { from, to }. On a Team Schedule Plus merchant each scheduled worker is texted.
GET/schedule-requestsStaff time-off, drop, pick-up and handoff requests (?status=pending). Plus feature.
POST/schedule-requests/{id}Decide: { decision: "approve" | "deny", note? }. The worker is texted. A handoff the coworker has not accepted yet returns 409.
FieldTypeDescription
worker_keyoptionalstring | nullu:<user_id> or c:<cashier_id> from the roster; null = open shift.
starts_at / ends_atoptionalISO datetimeEnd after start, at most 24 hours.
break_minoptionalintegerUnpaid break minutes, netted out of hours and labor cost.
roleoptionalstringPosition label shown to the worker (Server, Front desk).
statusoptionaldraft | publishedRead-only; flip with POST /shifts/publish.
↑ Request – what you send
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"
}'
GET /reports/summary · reports:read

Reports

Query
FieldTypeDescription
fromoptionalstringISO datetime start. Default 30 days ago.
tooptionalstringISO datetime end. Default now.
↑ Request – what you send
{ "object": "report_summary", "period": { "from": "…", "to": "…" }, "gross_sales": 128400, "transaction_count": 37, "currency": "usd" }
/statements · statements:read

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.

Query – GET /statements/pdf
FieldTypeDescription
yearrequiredintegerFour-digit year, e.g. 2026.
monthrequiredinteger1–12.
GET/statementsList available statement periods (period, year, month, pdf_url).
GET/statements/pdfDownload the statement PDF for a period (application/pdf). 404 if none.
↑ Request – what you send
curl -X GET https://www.merchant360.net/api/m360/v1/statements \
  -H "Authorization: Bearer m360_live_xxx"
GET · PATCH /settings

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

↑ Request – what you send
{ "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 · payments:*

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.

GET/custom-fieldsList field definitions.
POST/custom-fieldsCreate a field.
GET/custom-fields/{id}Retrieve a field.
PATCH/custom-fields/{id}Update a field.
DELETE/custom-fields/{id}Remove a field.
Body
FieldTypeDescription
labelrequiredstringShown to the payer (≤60 chars). The stable key is derived from it.
typeoptionalstring"text" (default) or "select".
optionsoptionalstring[]Choices for a "select" field (2–25).
requiredoptionalbooleanWhether the payer must fill it in.
surfacesoptionalobjectWhere it renders – payment_links, invoices, estimates, checkout, pay_widget, virtual_terminal (each defaults to true).
receiptsoptionalobjectWhere the captured value prints – customer_receipt (default false), merchant_receipt (default true).
accounting_classoptionalboolean"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.

↑ Request – what you send
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
  }
}'
/bnpl · bnpl:read / bnpl:write

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:

  1. Create a sessionPOST /bnpl/sessions for an amount. Klarna returns a client_token for the Klarna SDK; Affirm returns a public_key + script_url for affirm.js (Affirm has no server session).
  2. 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 an authorization_token.
  3. CapturePOST /bnpl/charges with that token. We authorize + capture at the provider and book the charge. Pass invoice_id to 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.

Session body – POST /bnpl/sessions
FieldTypeDescription
providerrequiredstring"affirm" or "klarna".
amountrequiredintegerCents (>0). Affirm's practical floor is $50.
invoice_numberoptionalstringShown on the Klarna order line. Defaults to "API".
customer.emailoptionalstringOptional – passed to Klarna as the billing email.
Charge body – POST /bnpl/charges
FieldTypeDescription
providerrequiredstring"affirm" or "klarna".
amountrequiredintegerCents (>0) – must match the amount the widget authorized.
tokenrequiredstringAffirm checkout_token / Klarna authorization_token from the widget.
invoice_idoptionalstringOptional – a Merchant360 invoice to mark paid with the same bookkeeping as the pay page.
customeroptionalobjectOptional name + email stored on the charge.
POST/bnpl/sessionsStart a checkout. Returns client_token (Klarna) or public_key + script_url (Affirm) for the widget.
POST/bnpl/chargesCapture the widget's token into a charge; optionally mark an invoice paid. Idempotent.
GET/bnpl/chargesList BNPL charges (newest first, paginated).
GET/bnpl/charges/{id}Fetch a single charge.
POST/bnpl/charges/{id}/refundRefund full (omit amount) or partial. Un-books any invoice it paid. Idempotent.
↑ Request – what you send
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 · vendor_payments:read / :write

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.

Payee – POST /vendor-payments/payees
FieldTypeDescription
namerequiredstringVendor legal / DBA name.
entity_typeoptionalstringBUSINESS (default) or INDIVIDUAL – drives SSN vs EIN on the 1099.
email / phoneoptionalstringContact info.
addressoptionalobjectline1, line2, city, state, zip – required to file a 1099.
tax_idoptionalstring9-digit SSN/EIN (encrypted at rest; last-4 returned).
is_1099_vendoroptionalbooleanWhether this vendor is 1099-reportable (default true).
bank_accountoptionalobjectrouting_number (9 digits, checksum-validated), account_number (4–17 digits), typeCHECKING (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.
Payment – POST /vendor-payments/payments
FieldTypeDescription
payee_idrequiredstringThe payee to pay.
amountrequiredintegerCents (>0).
memooptionalstringFree-text note (shown on the P&L expense).
payment_dateoptionalstringYYYY-MM-DD; defaults to today. Determines the 1099 tax year.
reportable_1099optionalbooleanCounts 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.

GET/vendor-payments/payeesList payees (with this-year totals).
POST/vendor-payments/payeesCreate a payee.
GET/vendor-payments/payees/{id}Get a payee.
PATCH/vendor-payments/payees/{id}Update a payee (partial; blank tax_id keeps the stored TIN, absent bank_account keeps the stored bank).
DELETE/vendor-payments/payees/{id}Archive a payee.
GET/vendor-payments/payees/{id}/w9W-9 PDF – ?type=blank (prefilled) or signed (stored).
GET/vendor-payments/payees/{id}/10991099-NEC recipient copy (PDF). ?year=YYYY.
POST/vendor-payments/paymentsRecord a payment (books the P&L expense). Idempotent.
GET/vendor-payments/paymentsList payments. ?payee_id= to filter.
GET/vendor-payments/payments/{id}Get a payment.
GET/vendor-payments/1099s1099 rollup for a tax year (JSON).
↑ Request – what you send
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 · tin_match:write

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

Body
FieldTypeDescription
namerequiredstringThe legal name on the TIN (business or individual).
tinrequiredstring9-digit SSN or EIN (dashes optional).
tin_typeoptionalstring"SSN" / "EIN" / "ITIN". Omit and the IRS resolves which file to check.
Response
FieldTypeDescription
objectoptionalstringAlways "tin_match".
matchedoptionalbooleantrue only on an exact name + TIN match (response codes 0, 6, 7, 8).
verdictoptionalstringMATCH / MISMATCH / NOT_ISSUED / INVALID_INPUT / DUPLICATE / ERROR.
response_codeoptionalnumber | nullThe raw IRS numeric code (see the table below). null when the request never reached the IRS.
descriptionoptionalstringPlain-language explanation of the code, safe to log or surface to staff.
tin_last4optionalstring | nullLast 4 digits of the submitted TIN, for your own reconciliation. The full TIN is never returned.
sourceoptionalstringLIVE when the IRS was queried, CIRCUIT_OPEN if our rate-limit guard short-circuited the call, DISABLED if matching is turned off.
checked_atoptionalstringISO 8601 timestamp of the check.
IRS response codes
response_codeverdictmatchedMeaning
0MATCHtrueTIN and Name combination matches IRS records.
1INVALID_INPUTfalseTIN is missing or not 9-digit numeric.
2NOT_ISSUEDfalseTIN entered is not currently issued by the IRS.
3MISMATCHfalseTIN and Name combination does not match IRS records.
4INVALID_INPUTfalseInvalid TIN Matching request.
5DUPLICATEfalseDuplicate request – the IRS caps repeats per 24h; we cache to avoid tripping it.
6MATCHtrueMatched on SSN (returned when tin_type is omitted/unknown).
7MATCHtrueMatched on EIN (returned when tin_type is omitted/unknown).
8MATCHtrueMatched 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.

↑ Request – what you send
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"
}'
iframe · /embed/vt · vtk_ keys

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>
Query params
FieldTypeDescription
keyrequiredstringThe embed key. vtk_test_… keys load directly for development; live vtk_… keys only work inside a registered domain (Referer-checked before any session exists).
headeroptionalstring1 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 · webhooks:manage

Webhooks

POST/webhooksRegister an endpoint (returns the signing secret, shown once).
GET/webhooksList endpoints.
DELETE/webhooks/{id}Remove an endpoint.
Body
FieldTypeDescription
urlrequiredstringHTTPS endpoint to receive events.
eventsoptionalstring[]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.succeeded

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

Event types
FieldTypeDescription
payment.succeededoptionaleventA card charge succeeded.
payment.authorizedoptionaleventA hold was placed (pre-auth).
payment.capturedoptionaleventA prior authorization was captured.
payment.voidedoptionaleventA hold or unsettled charge was voided.
payment.refundedoptionaleventA payment was refunded.
card.createdoptionaleventA reusable card was vaulted (save_card / POST /tokens).
invoice.createdoptionaleventAn invoice was created.
invoice.sentoptionaleventAn invoice was sent to the customer (emailed or texted).
invoice.paidoptionaleventAn invoice was paid in full.
invoice.voidedoptionaleventAn invoice was voided.
estimate.acceptedoptionaleventA customer e-signed and accepted an estimate.
subscription.createdoptionaleventA subscription started.
subscription.canceledoptionaleventA subscription was canceled.
ach.queuedoptionaleventAn ACH debit/credit was queued.
ach.settledoptionaleventAn ACH entry settled – funds moved. Your “funds are good” signal.
ach.returnedoptionaleventThe bank returned an ACH entry (payload carries return.code).
payment_link.paidoptionaleventA payment link was paid (instantly by card; on settlement by bank).
checkout.session.completedoptionaleventA customer paid a hosted Checkout Session.
webhook.testoptionaleventA 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.

↑ Request – what you send
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"
  ]
}'
7,000+ apps · no code

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.

One key, many merchants · for technology partners

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.

Header
FieldTypeDescription
M360-On-Behalf-OfrequiredstringThe 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.

Account fields
FieldTypeDescription
idoptionalstringThe merchant id – pass it as M360-On-Behalf-Of.
dba_nameoptionalstringThe merchant's business name.
linked_viaoptionalstringgrant (explicit access) or boarded (boarded under the partner's office).
realtime_data / volume_30doptionalobjectWith ?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:

FieldTypeDescription
Refunds / voidsoptionalguardrailBlocked when the merchant’s channel disables refunds (a void that only releases an authorization hold is still allowed).
ReportsoptionalguardrailA 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"] }'
↑ Request – what you send
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_…"
}'