In-Person Payments
24 min
beta in person payments (terminals) are in beta and not yet available in production these endpoints work in the demo/sandbox environment only; calling them in production returns 501 not implemented contact jetpay support mailto\ jetpay\@baselinepayments com for early access take a card present payment on a physical terminal and get the outcome in a single request use this flow for in person payments at a point of sale, counter, or kiosk, where the cardholder taps, inserts, or swipes their card on a terminal that jetpay manages for you you send a synchronous payment request to a terminal by its id, jetpay holds the request open while the cardholder pays, and the result comes back in the response to charge a stored card without a physical device, use the collect payment method at checkout docid\ lnp1zgf7ybpwuvs3g5urf flow instead before you integrate you will receive a physical terminal provisioned for your account before you start jetpay performs the terminal setup for you, so little to no configuration is required on your side to begin network setup your terminal may come with a sim card for mobile data if it does, it works out of the box, and wifi setup is only needed if you would rather not rely on mobile data to connect over wifi, open settings , tap network (this is passcode protected), enter the terminal passcode, then choose wi fi if you do not know your terminal passcode, please contact jetpay support mailto\ jetpay\@baselinepayments com connection strength matters on a weak signal or with network anomalies, requests can be slow to reach the terminal, time out, or fail to reach the terminal at all kiosk mode holding down on the spinner while the terminal starts up toggles kiosk mode on some devices, which controls the visibility of the settings button if this does not work on your device, contact jetpay support mailto\ jetpay\@baselinepayments com and we can make the change for you further customization if you would like terminal customization that we do not currently offer programmatically (for example, changing the terminal background), contact jetpay support mailto\ jetpay\@baselinepayments com and we can arrange it for you test cards use the test cards provided for the sandbox environment each test card carries multiple "applications" (different currencies and other details) to change which application is used, insert the card during a payment request the terminal then displays a list of applications to choose from unless dynamic currency conversion (dcc; disabled by default) is enabled, the current 'application' on a test card will have no effect on the terminal's payment request as such, if you encounter this screen, any 'application' you select for your card will be inconsequential at this time, the available test cards do not offer any cad applications (though this should not impact the implementation or testing of your integration) if you would like to enable dcc for your terminal, store, or company, please contact jetpay support mailto\ jetpay\@baselinepayments com to trigger simulated failures, request specific amounts https //docs adyen com/point of sale/testing pos payments#gift card test scenarios updating terminal configuration terminal configuration refreshes periodically if we make a configuration change for you and you want to confirm it has taken effect immediately, run a configuration update check on the terminal you can see the current state under settings > configuration , where status shows whether an update is available prerequisites getting your api key docid zgmim7qkrraox9jjaqrl at least one terminal provisioned to your account, belonging to a store a payout account configured on that terminal (funds are deposited here) terminals and stores are provisioned by jetpay before you charge, the terminal must exist and have a payout account; otherwise the request fails with a 409 payoutaccountmissing error use list your terminals # to see which terminals you can access and whether each has a payout account the terminal endpoints live on their own api reference page browse them at /docs/terminals https //extapi jetpay baselinepayments com/docs/terminals rather than the main /docs reference flow overview in the synchronous flow, your backend makes one request and waits for the terminal to return a result sequencediagram autonumber participant platform participant jetpay participant terminal participant cardholder platform >>jetpay post /terminals/{terminalid}/payment request (prefer respond sync) jetpay >>terminal prompt for payment cardholder >>terminal tap, insert, or swipe card terminal >>jetpay transaction result jetpay >>platform 200 ok (transaction result) implementation details list your terminals endpoint get /terminals https //extapi jetpay baselinepayments com/docs/terminals#tag/terminals (v0)/operation/get terminals terminals get returns every terminal you can access, with its store and payout account you will only need to call this to look up the jetpay uuid of each terminal you do not need to call it before every payment curl https //extapi demo jetpay baselinepayments com/terminals \\ x get \\ h "authorization bearer \<your api token>"import requests url = "https //extapi demo jetpay baselinepayments com/terminals" headers = { "authorization" "bearer \<your api token>" } response = requests get(url, headers=headers) print(response status code) print(response json())const fetch = require("node fetch"); const url = "https //extapi demo jetpay baselinepayments com/terminals"; fetch(url, { method "get", headers { "authorization" "bearer \<your api token>" } }) then(res => res json()) then(data => console log(data)) catch(err => console error(err)); a successful call returns http 200 with a list of terminals \[ { "poi id" "p400plus 123456789", "identifier" "b3f1c2d4 5e6f 7a8b 9c0d 1e2f3a4b5c6d", "effective payout account" "9f8e7d6c 5b4a 3210 fedc ba9876543210", "store" { "identifier" "a1b2c3d4 5e6f 7a8b 9c0d 1e2f3a4b5c6d", "name" "downtown store", "address" "123 main st, toronto, on" } } ] use the terminal's identifier (a uuid) as the terminalid path parameter in the requests below if effective payout account is null , contact jetpay support to configure one before you charge request a payment on the terminal endpoint post /terminals/{terminalid}/payment request https //extapi jetpay baselinepayments com/docs/terminals#tag/terminals (v0)/operation/post terminal payment request terminals terminalid payment request post send the amount to collect to a specific terminal synchronous is the default unless you set the prefer header to respond async (not covered by this guide), jetpay waits for the terminal and returns the result in the response you can send prefer respond sync explicitly to be unambiguous at minimum, provide the amount under paymenttransaction amountsreq requestedamount you can optionally shape the terminal prompt with transactionconditions (allowed card brands, preferred debit, display language, forced entry mode) and attach your own reference with saledata salereferenceid , which is echoed back in the result see the endpoint reference https //extapi jetpay baselinepayments com/docs/terminals#tag/terminals (v0)/operation/post terminal payment request terminals terminalid payment request post for the full request and header specification most merchants will want to supply their merchant category code (mcc) in transactionconditions merchantcategorycode it is optional, but recommended so the transaction is categorized correctly if you do not know your mcc, contact jetpay support mailto\ jetpay\@baselinepayments com once you send the request, the terminal prompts the cardholder for the amount idempotency include a unique idempotency key header (such as a uuid) so the request can be safely retried without charging the cardholder twice if a retry uses the same key, body, and headers, jetpay returns the outcome of the original request rather than starting a new payment reusing a key with a different body returns an error the exact retry behaviour for each transaction state is documented on the endpoint reference https //extapi jetpay baselinepayments com/docs/terminals#tag/terminals (v0)/operation/post terminal payment request terminals terminalid payment request post a synchronous request can run for a while the payment can be "advanced" at the terminal (for example, inserting a card then being prompted for a pin), and each step extends the payment's lifetime a request is capped at 150 seconds this endpoint allows up to 190 seconds for network delay and jetpay processing, so set your client's request timeout above 190 seconds, or it may be cut off before the terminal responds if a terminal has recently powered off, or powers off mid request, the request will almost always time out the payment provider does not check the terminal's status in real time, so the 'terminal unavailable' result will not immediately be returned to you in these cases curl https //extapi demo jetpay baselinepayments com/terminals/b3f1c2d4 5e6f 7a8b 9c0d 1e2f3a4b5c6d/payment request \\ \ max time 200 \\ x post \\ h "authorization bearer \<your api token>" \\ h "content type application/json" \\ h "prefer respond sync" \\ h "idempotency key 7c9e6679 7425 40de 944b e07fc1f90ae7" \\ d '{ "saledata" { "salereferenceid" "order 1042" }, "paymenttransaction" { "amountsreq" { "requestedamount" 10 00 } } }'import requests terminal id = "b3f1c2d4 5e6f 7a8b 9c0d 1e2f3a4b5c6d" url = f"https //extapi demo jetpay baselinepayments com/terminals/{terminal id}/payment request" headers = { "authorization" "bearer \<your api token>", "content type" "application/json", "prefer" "respond sync", "idempotency key" "7c9e6679 7425 40de 944b e07fc1f90ae7" } payload = { "saledata" { "salereferenceid" "order 1042" }, "paymenttransaction" { "amountsreq" { "requestedamount" 10 00 } } } \# allow more than 190 seconds for the terminal to respond response = requests post(url, json=payload, headers=headers, timeout=200) print(response status code) print(response json())const fetch = require("node fetch"); const terminalid = "b3f1c2d4 5e6f 7a8b 9c0d 1e2f3a4b5c6d"; const url = `https //extapi demo jetpay baselinepayments com/terminals/${terminalid}/payment request`; const payload = { saledata { salereferenceid "order 1042" }, paymenttransaction { amountsreq { requestedamount 10 00 } } }; // allow more than 190 seconds for the terminal to respond const controller = new abortcontroller(); const timeout = settimeout(() => controller abort(), 200000); fetch(url, { method "post", headers { "authorization" "bearer \<your api token>", "content type" "application/json", "prefer" "respond sync", "idempotency key" "7c9e6679 7425 40de 944b e07fc1f90ae7" }, body json stringify(payload), signal controller signal }) then(res => res json()) then(data => console log(data)) catch(err => console error(err)) finally(() => cleartimeout(timeout)); handle the response when the payment concludes, the endpoint returns http 200 with the full transaction result ( ipptransactionresult ) the example below is abbreviated response additionalresponse is a large map of provider specific key/value pairs, and paymentreceipt holds the full printable receipt content (typically both a cashierreceipt and a customerreceipt , each with many outputtext lines) sensitive values such as the terminal id are redacted here for the purpose of this guide, but will be available in the real response { "identifier" "33d57982 3f64 416b acb8 07652a8edca8", "response" { "result" "success", "additionalresponse" { "acquirerresponsecode" "approved", "cardscheme" "mc", "cardsummary" "9999", "fundingsource" "credit", "posauthamountcurrency" "cad", "posauthamountvalue" "10000", "posentrymode" "cless chip", "pspreference" "\<redacted>", "tid" "\<redacted>" } }, "saledata" { "salereferenceid" "order 1042" }, "paymentresult" { "paymentinstrumentdata" { "paymentinstrumenttype" "card", "carddata" { "paymentbrand" "mc", "maskedpan" "541333 9999", "entrymode" \["contactless"], "cardcountrycode" "826" } } }, "paymentreceipt" \[ { "documentqualifier" "cashierreceipt", "requiredsignatureflag" false, "outputcontent" { "outputformat" "text", "outputtext" \[ { "text" { "name" "total", "value" "cad 100 00", "key" "totalamount" }, "characterstyle" "bold", "endoflineflag" true }, { "text" { "name" "approved", "key" "approved" }, "characterstyle" "bold", "endoflineflag" true } ] } } ] } the http status reflects whether jetpay processed the payment request , not whether the card was charged a 200 means the transaction reached a conclusion always inspect response result for the actual payment outcome check response result for the outcome, success or failure on failure, response errorcondition explains why (see errors # ) on success, paymentresult carries the card details and paymentreceipt (when present) carries printable receipt content if the request runs long (303) a payment request can exceed the 150 second cap when the payment is advanced at the terminal (for example, a card is inserted and the terminal prompts for a pin, extending the payment's lifetime) when that happens, the endpoint responds with http 303 see other instead of 200 the body is an acknowledgement containing the transaction identifier , and the location header points at the status endpoint to poll location /payment requests/status?transactionid=d4e5f6a7 8b9c 0d1e 2f3a 4b5c6d7e8f90 poll that endpoint (see check payment status # ) until the transaction reaches a conclusion some http clients follow 303 redirects automatically, issuing a get to the location and discarding the original response body if desired, disable automatic redirect following for this request so you can read the 303 response body and location header yourself check payment status fetch the status of a payment request at any time this is how you resolve the 303 case # , and it is also your general fallback if anything interrupts the original request (an unexpected error, a dropped or interrupted connection, a client timeout), use it to recover the transaction's outcome by transaction id or idempotency key (recommended) get /payment requests/status https //extapi jetpay baselinepayments com/docs/terminals#tag/payment requests (v0)/operation/get ipp status payment requests status get with a query parameter, exactly one of transactionid or idempotencykey both are things you already have transactionid is the identifier returned by the request, and idempotencykey is the key you sent (useful when the request never returned an identifier ) latest request on a terminal get /terminals/{terminalid}/payment request https //extapi jetpay baselinepayments com/docs/terminals#tag/terminals (v0)/operation/get terminal payment request status terminals terminalid payment request get returns the same result for the most recent request on the terminal, and takes no query parameters the examples below use the recommended endpoint set your client timeout to at least 40 seconds for these calls curl "https //extapi demo jetpay baselinepayments com/payment requests/status?transactionid=d4e5f6a7 8b9c 0d1e 2f3a 4b5c6d7e8f90" \\ \ max time 40 \\ x get \\ h "authorization bearer \<your api token>"import requests url = "https //extapi demo jetpay baselinepayments com/payment requests/status" headers = { "authorization" "bearer \<your api token>" } \# provide exactly one of transactionid or idempotencykey params = { "transactionid" "d4e5f6a7 8b9c 0d1e 2f3a 4b5c6d7e8f90" } \# allow at least 40 seconds for this call response = requests get(url, params=params, headers=headers, timeout=40) print(response status code) print(response json())const fetch = require("node fetch"); // provide exactly one of transactionid or idempotencykey const params = new urlsearchparams({ transactionid "d4e5f6a7 8b9c 0d1e 2f3a 4b5c6d7e8f90" }); const url = `https //extapi demo jetpay baselinepayments com/payment requests/status?${params}`; // allow at least 40 seconds for this call const controller = new abortcontroller(); const timeout = settimeout(() => controller abort(), 40000); fetch(url, { method "get", headers { "authorization" "bearer \<your api token>" }, signal controller signal }) then(res => res json()) then(data => console log(data)) catch(err => console error(err)) finally(() => cleartimeout(timeout)); the response is the same ipptransactionresult shown above a 404 means no payment request exists for the identifier you provided the "latest request on a terminal" endpoint tracks payment requests from the last 48 hours only, and the "most recent" request may be a later failed one rather than the transaction currently on the terminal this behaviour is inherent to the payment provider, not jetpay when possible, fetch by transactionid or idempotencykey for an unambiguous result abort a payment request to cancel an in progress payment (for example, the cardholder walks away), abort it by transaction id or idempotency key (recommended) post /payment requests/abort https //extapi jetpay baselinepayments com/docs/terminals#tag/payment requests (v0)/operation/abort ipp payment requests abort post with a body containing exactly one of transactionid or idempotencykey latest request on a terminal post /terminals/{terminalid}/payment request/abort https //extapi jetpay baselinepayments com/docs/terminals#tag/terminals (v0)/operation/abort terminal payment request terminals terminalid payment request abort post aborts the most recent request on the terminal, and takes no body the "latest request on a terminal" abort has the same caveat as the status endpoint, and is likewise inherent to the payment provider, not jetpay the most recent request may be a later failed one rather than the transaction currently on the terminal when you know it, abort by transactionid or idempotencykey for an unambiguous target the examples below use the recommended endpoint set your client timeout to at least 40 seconds for these calls curl https //extapi demo jetpay baselinepayments com/payment requests/abort \\ \ max time 40 \\ x post \\ h "authorization bearer \<your api token>" \\ h "content type application/json" \\ d '{ "idempotencykey" "7c9e6679 7425 40de 944b e07fc1f90ae7" }'import requests url = "https //extapi demo jetpay baselinepayments com/payment requests/abort" headers = { "authorization" "bearer \<your api token>", "content type" "application/json" } \# provide exactly one of idempotencykey or transactionid payload = { "idempotencykey" "7c9e6679 7425 40de 944b e07fc1f90ae7" } \# allow at least 40 seconds for this call response = requests post(url, json=payload, headers=headers, timeout=40) print(response status code)const fetch = require("node fetch"); const url = "https //extapi demo jetpay baselinepayments com/payment requests/abort"; // provide exactly one of idempotencykey or transactionid const payload = { idempotencykey "7c9e6679 7425 40de 944b e07fc1f90ae7" }; // allow at least 40 seconds for this call const controller = new abortcontroller(); const timeout = settimeout(() => controller abort(), 40000); fetch(url, { method "post", headers { "authorization" "bearer \<your api token>", "content type" "application/json" }, body json stringify(payload), signal controller signal }) then(res => console log(res status)) catch(err => console error(err)) finally(() => cleartimeout(timeout)); a successful abort returns http 204 no content a 204 confirms only that the abort request reached the terminal , not that a payment was aborted confirm the final state with a status fetch # testing notes and limitations debitpreferredflag cannot currently be tested in the sandbox environment, because the payment provider does not offer test canadian debit cards allowedpaymentbrand does not currently work reliably in the sandbox environment due to a payment provider limitation, and can cause a transaction to auto reject once it reaches the terminal if you plan to use allowedpaymentbrand to disallow a card brand in production, contact jetpay support mailto\ jetpay\@baselinepayments com and we can configure it for you errors errors use the envelope { "error" "\<code>", "message" "\<detail>" } the full set of http status codes and error codes for each endpoint is listed in the api reference https //extapi jetpay baselinepayments com/docs/terminals one behaviour is easy to miss when a payment completes but the card is declined , the request still returns http 200 the http status only tells you jetpay processed the request; the payment outcome lives in the response body always branch on response result , and read response errorcondition (for example refusal , wrongpin , invalidcard , aborted ) when it is failure