Skip to main content
A payment is one charge of a payment method, either one you already created or one made in the same call from a session your page confirmed: a pay_test_… / pay_live_… object your server creates with a secret key. A card is captured in the same answer; a bank debit is processing until it settles, when the processor sends it to the bank in its next batch. The charge runs through the same engine as the rest of Payra, so it shows up in the dashboard, reconciles and refunds like any other payment.
1

Charge the payment method

Response
amount is in the smallest unit of the currency, so 12550 is $125.50, and it is charged exactly: the API adds no fee or surcharge. amount_refunded is how much of it has since gone back through refunds, wherever they were made. currency is USD or CAD; charge CAD only on a workspace Payra has set up to take Canadian dollars. description and reference are optional, the second being your own order or invoice number: it is stored as the payment’s order number and shown in the dashboard, and it does not apply the payment to a Payra invoice. When the method is saved on a customer and reference matches one of that customer’s prepayment orders (orders synced from your ERP through Payra Connect), the charge counts against that order: amount may not exceed what is still due on it, other pending charges included (400 parameter_invalid on amount), and a succeeded charge reduces what is due. customer, if you send it, must be the customer the method is saved on. Instead of payment_method, send payment_method_session, a session your page confirmed with Elements, within 30 minutes of the confirm: the payment method is made from it in this same call (one-time for a card) and comes back as payment_method. That is one call instead of two, the fastest checkout. Send one of the two, not both (or terminal instead, see Terminal payments). A session pays once: a retry with the same Idempotency-Key answers the same payment, and another key answers 400 payment_method_session_used. To charge a returning buyer’s saved card, find it first: see Customers. If the card’s bank wants to verify the customer, the charge says how: see 3-D Secure. To record what the customer bought, send line_items: see Order lines.
2

Read it back while it settles

A declined payment stays readable at the same id: the create answered 402, and this is where you see why.
An Idempotency-Key is required. The same key with the same body answers the same payment, including after a crash on our side, because the payment itself remembers the key. The same key with a different body is 400 idempotency_key_reused; after 24 hours only payment_method, amount and currency are compared (for a terminal payment, terminal, customer, amount and currency). What a payment costs you is not deducted from it. Processing fees are settled on your Payra statement, not per payment, because the card networks adjust interchange after the fact (a business card that qualified for a lower rate, say); amount is always what the customer paid, and a refund sends back exactly amount. Accounts that reconcile per order get processing_fee on the object, Payra’s estimate of that cost, or null: see Fees per payment. Charging a saved payment method emails the customer Payra’s payment receipt once the charge succeeds (for a bank debit, when the processor accepts the debit, before it settles), when the customer has an email address whose email notifications are on, the workspace’s Email payment receipts to customers setting is on, and its notifications are not paused. A one-time method has no customer, so its payment gets no receipt. A one-time payment method is spent by its first charge, approved or declined, and reads consumed afterwards. Only a request refused before it reaches the processor (a 400 on amount, currency or customer, payment_provider_not_configured, or a reused key) leaves it active. A saved one can be charged again.

Order lines

Send the order behind amount and Payra keeps it on the payment: line_items, one per product, and amount_details for the shipping, tax and discount around them. Both are optional; a payment sent without them works as before.
The lines must come to what you charge:
A line’s amount is quantity × unit_amount, rounded half up to the cent. Each line rounds on its own, and the sum adds the rounded lines: two lines of 1.5 × 333 are 499.5 each, so they come to 500 + 500 = 1000, not 999. A very small quantity can round to a line of 0. In the example, 9800 + 3150 + 899 + 1250 + 1059 − 1000 = 15158. Payra does not work out tax, shipping or discount: it adds up the ones you send. When they do not come to amount, the create answers 400 parameter_invalid on line_items with both sums in the message, and nothing is charged:
The request is refused before it is recorded, so send the corrected one with the same Idempotency-Key. If your own totals can be a cent off their parts (prices that include tax, or a unit price with a fraction of a cent, which unit_amount cannot carry), leave the lines out of that payment rather than adjusting them to fit. The payment returns them, each line with its amount (quantity × unit_amount, rounded half up to the cent), and so do its payment.* events. A payment made without them has "line_items": null and "amount_details": null.
Response (abbreviated)
On a card, where your processing account supports it, the lines also go to the processor as Level 3 data, the line detail that card networks ask for on business and purchasing cards: each sku becomes the line’s product code (capital letters and digits only, shortened to what the processor allows), shipping goes as a line of its own and tax on the charge. A line with a decimal quantity goes as quantity 1 at the line’s amount, so the processor’s line can differ from line_items; the payment keeps the quantity you sent. On a bank debit they are kept on the payment only. The hosted checkout takes the same two fields and lists them on its page.

Save the card with the payment

A buyer who ticks “save my card” at checkout needs no separate verification where the processor allows it: the payment proves the card. Charge the session, created with the buyer’s customer, and send save_payment_method: true:
Once the charge succeeds, Payra keeps the card on the customer’s file and the payment’s payment_method becomes usage: "saved": store that id for the buyer’s next checkout. The same works with a one-time payment_method you already created from the session. Sending the field is your attestation that the cardholder agreed to keep the card on file, as account_holder_authorization is when a card is saved without a payment.
  • A declined charge keeps nothing: the method stays one-time, and spent.
  • After 3-D Secure, the card is kept once the payment succeeds, within 30 minutes of the card being collected.
  • If the charge succeeded but the card could not be kept, the payment stands and the method stays one-time. Read the method back (GET /payment-methods/{id}) to see which.
  • A method that is already saved ignores the field.
  • If the customer already had this card saved, it is not saved twice: the payment’s payment_method is the method that holds it, and the one-time method made from the session stays spent. Store that id as usual.
  • To save a card without charging it, as on an “add a card” page, create the method with account_holder_authorization: Payra verifies the card first.

Bank debits

A bank account is charged for what its holder authorized. A one-time account takes exactly the amount and currency its session named, else 400 parameter_invalid on amount; a saved one takes the amount you send. currency is USD for a US account and CAD for a Canadian one. The answer is a processing payment with payment_method_type: "us_bank_account" and a us_bank_account block (last4, account_type, account_holder_type), card being null:
It becomes succeeded when it settles, once the processor’s next batch sends it to the bank, and succeeded_at is set then; the bank can instead refuse the debit, which turns it returned with failure.code: "ach_returned", or the processor can reject it outright, which turns it failed. Listen for payment.succeeded, payment.returned and payment.failed rather than polling, and do not ship on processing. Every debit carries the authorization evidence Payra recorded when the holder accepted it, so a dispute can be answered from Payra’s records.

When a debit settles, and when it can come back

A bank debit moves on the ACH network’s own clock; there is no instant rail, and the create never answers succeeded for one.
  • Settlement. The processor sends its batch to the bank once a banking day. A debit accepted before that day’s cut-off goes out that night and turns succeeded then; one accepted after it waits for the next batch. Weekends and bank holidays are not banking days, so a debit taken on a Friday evening settles on Monday at the earliest.
  • Returns. After settlement the bank can still send the debit back, which turns the payment returned with failure.code: "ach_returned" and sends payment.returned. Under the network’s rules, most returns (no such account, closed account, insufficient funds) arrive within two banking days of settlement; a holder disputing the authorization on a personal account has up to 60 calendar days. Treat succeeded on a debit as “very likely”, not “done”: release what you are shipping on the business risk you are comfortable with, and reverse it on payment.returned.
  • Nothing to configure. The schedule is the processor’s; Payra reports each step as it happens. Accounts with a funding arrangement of their own still see the same statuses at the same moments.

Canadian bank debits (EFT)

A Canadian account (a session of type acss_debit) is debited under the pre-authorized debit (PAD) agreement its holder accepted, in CAD only, and follows the same lifecycle. The payment carries payment_method_type: "acss_debit" and an acss_debit block, with the branch the account belongs to, in place of us_bank_account:
It turns succeeded once the processor settles it, failed when the processor rejects it, and returned with failure.code: "acss_returned" when the bank sends it back, before or after it settled. Canadian debits are set up per workspace: where they are not, the session is refused at creation with 400 payment_provider_not_configured, so no customer reaches the fields.

3-D Secure

The card’s bank can ask to verify the customer before a card charge goes through, on a workspace where Payra has turned 3-D Secure on. The charge then answers 201 with nothing captured yet, status: "requires_action" and a next_action that says what to do. Where Payra runs 3-D Secure itself, on a card confirmed with Elements, it runs whether or not you send return_url; return_url only decides how the customer goes through it:
  • Without return_url, the steps run on your page, in frames: Payra Elements does it in one call.
  • With return_url, the customer goes to a page and comes back to that address: for pages that can only send the customer somewhere, as the WooCommerce plugin does.
Where the card’s processor runs it instead (a wallet, a card confirmed without Elements, or a workspace Payra set up that way), only a charge that sends return_url can be verified, by redirect. Sending return_url whenever the customer is there works in both cases.

On your page

A card confirmed with Elements carries the customer’s browser, and a charge without return_url answers with a Payra page for your page to show:
Response (abbreviated)
next_action.type is three_ds_fingerprint (the bank looks at the browser, nothing to see) or three_ds_challenge (the bank asks the customer something); url is a page of Payra’s that your page runs in a frame. Elements does all of it in one call, which also does nothing for any other status:
The customer never leaves your page. Your server must hand a requires_action payment to your page: a server that neither sends return_url nor calls handleNextAction leaves the payment waiting, and a step nobody answers fails it after 15 minutes. Charge while the customer is still on your page, for the same reason: a card confirmed with Elements is verified on any charge, one your server makes later included. Read the outcome on your server (GET /payments/{id}, or the payment.succeeded / payment.failed webhooks): the page’s word is never what confirms an order.

By redirect

Send return_url, the address your customer comes back to afterwards:
When the bank wants to verify the customer, the charge answers 201 with nothing captured yet and a page to send the customer to: Payra’s own, which runs the bank’s steps and shows its challenge, or the bank’s.
Response (abbreviated)
When it does not, the answer is succeeded or a decline.
1

Send the customer to the page

Hand the payment to your page and send the customer to next_action.url. With Elements that is one line, which does nothing for any other status:
2

The customer comes back

After the bank, the customer returns through Payra, which settles the payment, and lands on your return_url with ?payment=pay_… appended. A customer who gives up on the way lands there too, with the payment failed or still requires_action.
3

Confirm on your server

Read GET /payments/{id}, or wait for payment.succeeded / payment.failed. Do not rely on the landing alone: anyone can open your return_url.
  • return_url must be https (a sandbox key may also use http://localhost while you build), at most 2048 characters.
  • A verification the customer does not finish fails after 15 minutes, with failure.code: "authentication_failed" and a payment.failed event; so does one the customer closes or cancels. requires_action itself has no event.
  • A one-time payment method stays spent: after a failed verification, collect the card again with a new session.
  • Saved cards are verified the same way when the customer is there. Leave return_url out of a charge made with nobody at the keyboard, a scheduled charge say: there is no one to send to a page.
  • return_url has no effect on a bank debit, nor on a workspace where Payra has not turned 3-D Secure on: the charge runs as if it was not sent, so one integration works on every workspace. Ask Payra to turn it on for yours.
  • A retry with the same Idempotency-Key replays the first answer, next_action included; GET /payments/{id} reads the payment as it is now.
  • The hosted checkout does all of this for you.

Terminal payments

Send terminal instead of payment_method or payment_method_session to charge a card presented on one of your card terminals. Send exactly one of the three.
  • customer is required, currency is USD, and return_url and save_payment_method are refused. A terminal that is not yours, not assigned yet, or in the other environment answers 400 parameter_invalid on terminal. Nothing is created, so the same Idempotency-Key works once the terminal is assigned.
  • The answer is 201 with a processing payment: payment_method is null, payment_method_type is card_present, terminal names the terminal and card is null. On every other payment terminal is null.
  • The outcome comes later: succeeded once the card is approved, canceled with a cancellation_reason, or failed with failure. A declined card may not end the payment: the terminal shows the decline and lets the customer try another card, and the payment can stay processing. Where the processor records the decline, the payment ends failed instead. If nobody pays, it ends canceled with cancellation_reason: "expired" 5 minutes after it was sent.
  • Keep the payment open until its outcome comes from Payra: the payment.succeeded, payment.failed or payment.canceled webhook, or a read-back whose status is no longer processing. The customer needs time to reach the terminal and present the card, and a canceled payment cannot take a card. Do not cancel after a short wait or on a timer of your own: the 5-minute expiry already covers a payment nobody pays (with the exceptions below). Cancel only when the customer or your user asks to stop (see Cancel a terminal payment).
  • The terminal shows the card approved a few seconds before Payra sends the outcome. Until the outcome comes from Payra, show the payment as waiting in your app, and treat it as paid only once it is succeeded, never on what the terminal’s screen shows. There is no separate status for a card being read: the payment stays processing while the terminal waits for the card and while it reads it, and terminal_progress usually says which (see Where the customer is). If it is still processing long after the terminal showed approval, read it back before charging the customer again on another terminal.
  • A refusal by the processor at the moment the payment is sent, a terminal the processor could not reach, or a payment Payra could not record and stopped before a card was read, answers 402 card_declined with decline_code: "processing_error". No card was read: check the terminal and retry.
  • If Payra loses the processor’s answer, or the processor fails with a server error, the payment stays processing until the outcome is known. It is never reported as a decline. Until then it does not expire, cannot be canceled, and its terminal answers terminal_busy.
  • A second payment to a terminal still on one answers 400 terminal_busy and creates nothing; the same Idempotency-Key works once the terminal is free.
  • A reference naming a prepayment order is capped at what is still due on it, as on any payment.
  • line_items are kept and shown back on the payment, but are not sent to the card networks for terminal payments.

Where the customer is

While a terminal payment is processing, GET /payments/{id} also asks the terminal which step it is on and answers it as terminal_progress, so your screen can say more than “waiting”:
  • Poll GET /payments/{id} no faster than once a second. Polling faster rarely shows a newer step: each Payra server asks a terminal at most once a second per payment and answers the same step in between. Every poll counts toward your key’s read limit, so with many terminals in flight at once, poll each less often.
  • The read waits up to three seconds for the terminal, so allow for that in your client’s timeout. It never fails because of the terminal: when the terminal does not answer, the payment comes back with terminal_progress: null.
  • It is the terminal’s step at the moment of the read. Nothing is stored, so a step shorter than your polling interval is never seen.
  • The outcome is still the payment’s status. terminal_progress never says approved or declined, and it is null once the payment has left processing.
  • It is also null on the create, in lists and in webhook events, for the first three seconds after the payment is sent, while a cancel is running, when the terminal did not answer within three seconds or gave no answer (the server that asked then leaves it alone for five), on a step that does not place the customer (a receipt printing), in the moments after the terminal finished and before Payra has the outcome, and on a terminal payment Payra routes through a processor that does not report steps. A null is never a step of its own: keep showing the last value you read.
  • A cancel sent while one of these reads is at the terminal waits for it first, three seconds at most.
  • More state and prompt values may be added. Show one you do not know as still processing.

Cancel a terminal payment

Cancel only when a person decides not to pay: the customer says so, or your user stops the sale in your app. Never cancel because the outcome has not come after a few seconds: the customer may still be reaching for the card, and the 5-minute expiry already covers a payment nobody pays.
While the terminal waits for the card, the payment answers canceled with cancellation_reason: "requested", and the terminal is free for the next payment. Canceling one already canceled answers it as it is. Once a card was presented (the terminal refuses the cancel, or already has the card’s transaction), or on any payment that is not a terminal payment, the answer is 400 payment_not_cancelable and the payment finishes on its own. 409 conflict means the cancel could not be confirmed and the payment is still waiting: it changed while the cancel ran, or the terminal’s answer was lost. The payment was not marked canceled, and the same Idempotency-Key, if you sent one, can be retried: the retry answers the payment as canceled, or payment_not_cancelable once the card was presented.

Status

status is Payra’s own lifecycle. It is never a string from the processor.

Why a payment failed

failure is set when status is failed or returned, and null otherwise. It carries a code from a fixed vocabulary, a reason when the processor says exactly what it refused, and a message written by Payra. The processor’s own wording is never relayed, so you can show or log message safely and branch on code, then on reason where you want a finer sentence. failure.reason is null unless the processor names the cause. More values may be added. message follows the reason, except for card_blocked, restricted_card and suspected_fraud, which keep the plain decline wording: tell the customer the card was declined, not why the bank refused it.

Declines

A refusal is an answer, not an outage: the create responds 402 card_declined and leaves the payment behind for you to read. Testing lists the sandbox cards that produce each one.
code is card_declined for every refusal, so decline_code is what you branch on. When the processor names the cause, decline_reason carries it, the same value as failure.reason; it is absent otherwise. decline_code uses the same vocabulary as failure.code, which means it can be processing_error: that one is not the issuer refusing the card, and it is worth retrying with a new Idempotency-Key (the same key replays this 402). A saved method can simply be charged again; a one-time method was spent by the attempt, so collect the card again with a new session. A charge the processor refuses, or that Payra could not send at all (the workspace has no processor account for that currency, say), answers this same 402, never a 500: the failed payment exists, reads back and lists. When Payra cannot tell whether the processor received the charge (the request timed out, or the connection failed), the create answers 201 with status: "processing" instead, and a later webhook says how it ended.

Fraud screening

Fraud screening is not a separate service or a separate call. Payra is both the gateway and the processor of the payment, so every card payment is scored by the risk engine synchronously, inside the authorization itself, on every workspace, with nothing to turn on and nothing to send. The payment answers with its outcome already screened. A payment the screening blocks is a decline like any other: 402 card_declined, with decline_reason: "suspected_fraud" when the processor says so, and the payment reads back failed with the same failure. The bank’s own fraud rules answer the same way, so your code handles one case, not two. For a check the customer takes part in, add 3-D Secure: it shifts liability for fraud to the card’s bank on the payments that complete it.

Listing payments

GET /payments answers the payments your keys have made, newest first, a page at a time. It takes limit and starting_after, and works the same as every other list; see pagination for walking the pages.
Response (abbreviated)
The list only ever holds your own workspace’s payments, in the environment of the key you sent. The list takes no filters; when you already know an id, read it with retrieve.

Errors