← All guides

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.
Your POS sends an amount; the reader at that station wakes up, takes the card, and you read the result. Readers sit in POS Listener mode: a waiting screen with the merchant’s logo and yours, and nothing else for the clerk to press.
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

Electronic Payments does thisOnce

Electronic 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 /accounts at 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.

2

Keep the key on your server

RequiredOnce

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

In the API referenceStoring the platform key →
3

Send these headers on every call

RequiredEvery call
  • Authorization: Bearer m360_isv_…
  • M360-On-Behalf-Of: the merchant the call is for: its id from GET /accounts, or its MID. Never the business name (names repeat). Not sent on GET /accounts.
  • Idempotency-Key on every POST (your order or ticket id).

Base URL: https://www.merchant360.com/api/m360/v1

In the API referenceAuthentication →
4

Build against the sandbox

RequiredBefore go-live

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

In the API referenceTest mode →Developer kit →
5

Find the merchant

RequiredOnce per store

GET /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" } ] }
In the API referenceGET /accounts →
6

Map each reader to a POS station

RequiredOnce per store, and when a reader changes

GET /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" } ] }
In the API referenceReaders (devices) →
7

Send the sale to the reader

RequiredEvery sale

POST /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 }
In the API referenceSale →Payments →
8

Get the result

RequiredEvery sale

Ask for the payment every 2 to 3 seconds until its status is no longer pending:

  • succeeded: approved. amount is what was charged (with any tip or fee the merchant’s pricing adds on the reader); card has 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" } }
In the API referenceStatuses →
9

Cancel, void, refund, tip

RequiredWhen the clerk asks
  • POST /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. Send amount for part of it.
  • POST /payments/{id}/tip-adjust: add or change a tip on an approved sale.
10

Handle a lost answer

RequiredEvery POST

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

In the API referenceIdempotency →Recovery →
11

Per-sale options

OptionalAny sale
  • terminal_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/authorize with the same body: a hold instead of a sale, captured later with /payments/{id}/capture or released with /void. Needs pre-auth on the merchant.
12

Get results pushed to you

OptionalOnce

Polling 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 }
In the API referenceWebhooks →
13

Show the error code

RequiredAny refused call

Every 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.
In the API referenceErrors →
14

Go live

RequiredOnce per merchant
  1. With the platform key: GET /accounts shows the merchant; GET /terminal/devices shows its readers.
  2. A $1.00 sale (amount: 100) on one reader; tap a card; read it as succeeded.
  3. 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.

SettingWhat it decidesWhen off
Connected merchantsWhich merchants your key reaches.403 no_grant
API scopesWhich kinds of call your key may make.403 insufficient_scope
Reader modePOS Listener (waiting screen only), Integrated or Standalone.
Receipt for sales sent from the POSTerminal: 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 salesWhether allow_keyed shows Key enter card.422 moto_not_enabled
Pre-authWhether POST /payments/authorize works on the reader, and its maximum.422 preauth_not_enabled
RefundsWhether a partner may refund and void.403 not_permitted_by_channel
Real-time dataWhether 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 ToolsThe merchant’s Merchant360 switch.403 billing_tools_off
Allowed IP addressesOptional: locks your key to your servers’ fixed addresses.403 ip_not_allowed