A POS for many merchants (ISV)
How do I integrate my POS, used by many merchants, with card readers?
- For
- A software company (ISV) whose POS runs at many merchants, each with one or more card readers running the Merchant360 Payment App.
- Key
- One platform key (m360_isv_…) on your server.
POS station -> your server (platform key) -> Merchant360 API -> the station's reader
result <- GET /payments/{id} (or a webhook)- 1. Get set up with Electronic Payments(we do this)
- 2. Keep the key on your server
- 3. Send these headers on every call
- 4. Build against the sandbox
- 5. Find the merchant
- 6. Map each reader to a POS station
- 7. Send the sale to the reader
- 8. Get the result
- 9. Cancel, void, refund, tip
- 10. Handle a lost answer
- 11. Per-sale options(optional)
- 12. Get results pushed to you(optional)
- 13. Show the error code
- 14. Go live
Get set up with Electronic Payments
Electronic Payments does thisOnceElectronic Payments creates your partner record and sends your platform key through a secure share (shown once). Then, for each merchant:
- We connect the merchant to you. It appears in
GET /accountsat once. - We register and activate its readers, and set them to POS Listener.
- We set what the merchant allows: keyed sales, pre-auth, refunds, tips, and the receipt rule (below).
You send us your logo (PNG, JPEG or WebP, up to 2 MB, wide, transparent background) for the bottom of the waiting screen.
Keep the key on your server
RequiredOnceStore it in a secrets manager or your host’s encrypted environment variables and read it when the service starts. Never in the POS app, on a store PC, on the reader, in source control, chat, email or logs: anyone holding it reaches every connected merchant.
Each POS station calls your server; your server calls Merchant360. No server of your own? Use the POS with no server guide instead.
Send these headers on every call
RequiredEvery callAuthorization: Bearer m360_isv_…M360-On-Behalf-Of: the merchant the call is for: its id fromGET /accounts, or its MID. Never the business name (names repeat). Not sent onGET /accounts.Idempotency-Keyon every POST (your order or ticket id).
Base URL: https://www.merchant360.com/api/m360/v1
Build against the sandbox
RequiredBefore go-liveGet a free test key at cygma.cloud/merchant360/sandbox. With a test key any terminal_id works, GET /payments/{id} approves the sale (amount: 4100 declines, to test a decline), and no money moves. A test key belongs to one test merchant, so leave M360-On-Behalf-Of out while testing.
Find the merchant
RequiredOnce per storeGET /accounts lists every merchant connected to you with its id. Save the id with the store in your setup. This is a lookup, not part of a sale.
GET /accounts
Authorization: Bearer m360_isv_…
200 { "object": "list", "data": [ { "id": "MERCHANT_ID", "dba_name": "LIFELONG", "linked_via": "grant" } ] }Map each reader to a POS station
RequiredOnce per store, and when a reader changesGET /terminal/devices lists the store’s readers. Save which terminal_id sits at which POS station (the serial is on the label under the reader). A good home for both lookups is an “Import card readers” button in your store setup.
GET /terminal/devices
Authorization: Bearer m360_isv_…
M360-On-Behalf-Of: MERCHANT_ID
200 { "available": true, "default_terminal_id": "PAXA35/2290000001",
"devices": [ { "terminal_id": "PAXA35/2290000001", "name": "Front counter", "model": "PAXA35", "serial": "2290000001" } ] }Send the sale to the reader
RequiredEvery salePOST /payments with the amount in cents and the terminal_id of the reader at that station. Merchant360 hands the sale to that reader: it leaves its waiting screen, shows the amount and asks for the card (tap, chip or swipe). Card data never reaches your software.
Always send terminal_id: it is how Merchant360 knows which reader to wake. Left out, the sale goes to the merchant’s default reader, which is wrong for a store with two stations.
You get 202 at once, with status: "pending" and a payment id starting tp_. Save the id.
POST /payments
Authorization: Bearer m360_isv_…
M360-On-Behalf-Of: MERCHANT_ID
Idempotency-Key: T-1001
{ "amount": 1800, "terminal_id": "PAXA35/2290000001", "invoice_number": "T-1001" }
202 { "id": "tp_5b0e…", "object": "payment", "status": "pending", "amount": 1800 }Get the result
RequiredEvery saleAsk for the payment every 2 to 3 seconds until its status is no longer pending:
succeeded: approved.amountis what was charged (with any tip or fee the merchant’s pricing adds on the reader);cardhas the brand and last 4.failed: declined.canceled: cancelled on the reader, or by you.
A sale is never cancelled on a timer. One still pending after 360 seconds is flagged for review and stays pending.
GET /payments/tp_5b0e…
M360-On-Behalf-Of: MERCHANT_ID
200 { "id": "tp_5b0e…", "status": "succeeded", "amount": 1800,
"card": { "brand": "VISA", "last4": "4242" } }Cancel, void, refund, tip
RequiredWhen the clerk asksPOST /payments/{id}/cancel: stop a sale still waiting for the card. If the card was charged just before, it is voided.POST /payments/{id}/void: undo an approved sale the same day, before the batch closes.POST /payments/{id}/refund: money back after the batch closed. Sendamountfor part of it.POST /payments/{id}/tip-adjust: add or change a tip on an approved sale.
Handle a lost answer
RequiredEvery POSTSend an Idempotency-Key (your order or ticket id) on every POST. If the answer is lost (timeout, restart, Wi-Fi drop), send the same POST again with the same key: you get the same payment back, never a second sale. Then read its status as in the previous step.
Per-sale options
OptionalAny saleterminal_options.allow_cancel: false: no Cancel Sale button on the reader for this sale.terminal_options.print_receipt: true: the reader prints or offers the receipt for this sale, for a register with no printer.false: your POS prints and the reader goes back to its waiting screen. Left out: the terminal’s setting (Payment App 2.46.4 or later).terminal_options.allow_keyed: true: a Key enter card button, for a card that will not read. Needs keyed (MOTO) sales on the merchant; the reader then asks for CVV, ZIP and street number by default.tip: { prompt: true }: the tip screen on the reader.POST /payments/authorizewith the same body: a hold instead of a sale, captured later with/payments/{id}/captureor released with/void. Needs pre-auth on the merchant.
Get results pushed to you
OptionalOncePolling alone works. A webhook adds three things: a result still reaches you when your request or POS dropped; refunds and voids done outside your POS (in Merchant360 or on the reader) reach you; and one endpoint covers every merchant without spending your 1,200 requests a minute on polling.
Register once with POST /webhooks using the platform key. Keep the secret it returns (shown once) and check the M360-Signature header on each delivery. Answer 2xx fast; the same event can arrive twice, so handle it by its id.
POST /webhooks
Authorization: Bearer m360_isv_…
{ "url": "https://your-server.example.com/m360-webhook" }
201 { "id": "…", "secret": "whsec_…", "enabled": true }Show the error code
RequiredAny refused callEvery error is { "error": { "message", "code" } }. Most refusals name a setting, not an outage: show the code to your user instead of “contact support”.
403 no_grant: the merchant is not connected to you.403 insufficient_scope: your key is not granted that kind of call.403 billing_tools_off: the merchant’s Billing Tools are off.403 not_permitted_by_channel: reports or refunds are off for partners on that merchant.409 terminal_unavailable: no card readers on the account.422 moto_not_enabled/422 preauth_not_enabled: keyed sales / pre-auth are off for the merchant.429 rate_limited: too many requests in the minute.500: ours. Send us the time and your reference.
Go live
RequiredOnce per merchant- With the platform key:
GET /accountsshows the merchant;GET /terminal/devicesshows its readers. - A $1.00 sale (
amount: 100) on one reader; tap a card; read it assucceeded. POST /payments/{id}/void, then send us the payment id so we confirm it on our side.
Settings Electronic Payments controls
Ask us when you need one changed. Your integration sees each as the code beside it.
| Setting | What it decides | When off |
|---|---|---|
| Connected merchants | Which merchants your key reaches. | 403 no_grant |
| API scopes | Which kinds of call your key may make. | 403 insufficient_scope |
| Reader mode | POS Listener (waiting screen only), Integrated or Standalone. | |
| Receipt for sales sent from the POS | Terminal: the reader offers its receipt. POS: the reader shows the result for 2 seconds and returns to its waiting screen; your POS prints or sends the receipt. Use POS for readers without a printer, such as the A35 (Payment App 2.43.31 or later). A sale can override it with terminal_options.print_receipt, and the merchant can change it on the terminal (2.46.4). | |
| Keyed sales | Whether allow_keyed shows Key enter card. | 422 moto_not_enabled |
| Pre-auth | Whether POST /payments/authorize works on the reader, and its maximum. | 422 preauth_not_enabled |
| Refunds | Whether a partner may refund and void. | 403 not_permitted_by_channel |
| Real-time data | Whether a partner may read the transactions feed, the report summary and volume. Each sale’s own result never needs it. | 403 not_permitted_by_channel |
| Billing Tools | The merchant’s Merchant360 switch. | 403 billing_tools_off |
| Allowed IP addresses | Optional: locks your key to your servers’ fixed addresses. | 403 ip_not_allowed |