When a payment is interrupted

A customer loses the page part-way through paying. The connection drops, the browser crashes, they press back, they close the tab during their bank's verification step. Two questions follow, and they have different answers: what already exists in Shuttle, and whether trying again can take the money twice.

This page is for the platform embedding the checkout. It describes what Shuttle does on its own and the one thing you have to get right yourself.

What exists, and when

Most of the confusion about interrupted payments comes from assuming records appear when the payment succeeds. They do not. By the time a connection can drop mid-payment, almost everything already exists.

When you prepare the checkout, before the customer has seen anything:

  • The account is resolved or created. An unknown crm_key creates one. A customer session is created for it.
  • The checkout request is cached against its nonce.

When the customer presses pay, and still before the gateway is called:

  • An agreement and a contract are created. The contract is in setup, and a contract in setup is excluded from every contract list, so it will not appear in the merchant's screens or in your list calls.
  • A payment method record is created from the values entered.
  • A transaction is written with status UNRESOLVED, and a status check is scheduled ten minutes out.

Only then is the gateway called, and its answer rewrites that transaction.

The practical consequence: an account with a contract in setup and an UNRESOLVED transaction is not evidence that money moved. It is the normal state of a payment that is a fraction of a second old, and it is also the state left behind when the connection dies at the worst moment. The transaction's status, once the gateway or the scheduled check has answered, is what tells you which.

The shapes an interruption can take

Straight through. The whole submission is one HTTP call: the gateway is asked and answers within it. An interruption here is a lost response, not a lost payment. The gateway has already decided.

Bank verification or a gateway-hosted page. The gateway answers with a URL instead of a result, and the customer leaves for their bank or the provider's own card page. This is the longest window, and the one customers actually abandon. When they return, the return handler re-checks the nonce for a duplicate before completing anything, and the transaction it completes must still be unresolved, awaiting authentication, or failed: one that already succeeded cannot be completed a second time. If they never return, the transaction stays as it was and the scheduled check resolves it.

Delayed settlement. Bank debits and some wallets answer PENDING rather than approving. The payment is attributed to its charge straight away even though the money has not arrived, which is what stops anything else collecting the same amount. The scheduled check resolves it when the gateway confirms. Waiting, not retrying, is what finishes these.

No answer at all. The transaction stays UNRESOLVED. Ten minutes later Shuttle asks the gateway whether the payment exists, for gateways that support being asked: if the gateway has never heard of it the transaction is failed as a retryable decline and nothing was taken; if the gateway has it, the real outcome is written and attributed as though it had come back in time; if the gateway cannot be reached the check tries again five minutes later. Where a gateway does not support the question, the transaction stays UNRESOLVED until somebody looks. That is the one outcome that needs a person.

Nothing due yet. A scheduled date, a delayed start, or a schedule whose first charge is in the future takes no payment at checkout at all. An interruption here cannot have taken money, whatever else it left behind.

How Shuttle stops it being paid twice

This is layered rather than a single check, because the same payment can arrive twice in several different ways.

Two submissions racing each other:

  • The nonce is locked for the duration of a submission, with no wait. The second submission fails immediately rather than queueing behind the first and running when it finishes.
  • Only one payment runs on a contract at a time. A second waits up to 30 seconds for the contract lock, and the checkout form, told the payment is already in progress, keeps retrying for up to five minutes rather than showing an error.
  • Every operation that can charge a contract takes the same lock, so an automated collection and a payment you send cannot run alongside each other.
  • Repeated submissions using the same saved payment method are serialised for 60 seconds, and a second within 5 seconds is given the first one's answer rather than being run again.

A payment that already happened:

  • On submission, a contract created on the same nonce within the last 24 hours that holds a successful, pending, unattributed or unresolved transaction is a duplicate. Nothing is taken and the answer names the contract.
  • For a recurring agreement the test is wider: any contract on that nonce that has left setup counts, even with no transaction behind it.
  • Preparing a second checkout on a nonce that is already bound to a contract is refused, so the customer never reaches a payment form they should not see.
  • The return from bank verification runs the duplicate check again before completing, and sends the customer to your cancel URL with the reason rather than authorising.

Money already committed:

  • An unresolved transaction's amount is subtracted from what the contract has outstanding, and a pending one is already attributed to its charge. A second attempt therefore finds nothing due rather than finding the same amount due twice.
  • A caller that waited for the contract lock re-reads the contract inside the lock before acting, so it cannot act on what it saw before it waited.

And two protections that work the other way, so a nonce is not spent on nothing:

  • A declined payment releases the nonce. The customer retries with the same value. Only a payment that succeeded or is still in flight holds it.
  • An agreement that fails after the contract exists but before any transaction does releases it too, so a payment link is not burned by a failure that took no money.

What the checkout does with all this

If the customer reloads and lands back on the same checkout, the form asks whether that nonce already has a contract, before they can press anything. If it does, they see "This payment has already been processed." and a button that takes them to their receipt; paying is disabled. This does not apply when the checkout is only saving a card, or when fast checkout is on.

If they press pay on something that has already gone through, the answer identifies the contract, the payment and the receipt, and the form moves them straight to the receipt, or closes to your success URL if your request asked to skip it.

What the checkout cannot do is recognise a reload that came back through your page with a different nonce. At that point it is a new payment, and it will be taken. Which is the next section.

The one thing you have to get right

Everything above keys off the nonce. Shuttle will not take a payment twice for the same order provided the second attempt carries the same nonce as the first.

So do not let Shuttle generate it. Send a nonce derived from the thing being paid for: the order id, the invoice reference, the policy number, the booking. Stable across a reload, unique to that one payment.

The reason is where a reload goes. It goes to your page, not to Shuttle. Your page creates a checkout again, and if that second create carries a fresh value there is nothing for Shuttle to recognise. A nonce derived from an order also survives the customer coming back an hour later, closing the laptop and finishing on a phone, or somebody picking up where they left off.

It may contain letters, digits, underscores and hyphens. A slash, a space or a colon means it is not treated as a nonce at all, so shape your reference accordingly.

If you sign the payment request yourself rather than creating the checkout through the API, the nonce must be in the signed request. Leave it out and one is generated from the clock in milliseconds, different on every load, so every reload is a new payment and none of the protection above applies. Nothing warns you; the payment simply succeeds twice.

One limit either way: the duplicate check looks back 24 hours, for every kind of nonce. It covers the customer who reloads, comes back after lunch, or finishes on their phone. It does not cover the same order being paid again next week, and it does not stretch for a payment link even though the link's own binding is kept far longer.

Reading the outcome

When you or a merchant need to know what happened, the transaction's status is the only thing that says so, and each one means something different about what is safe to do next:

  • Successful. The money moved. Anything else taken now is a second payment.
  • Pending. The gateway has it and has not finished. It will resolve on its own. Taking another payment now creates a real duplicate.
  • Unresolved. The attempt ended without a definite answer and Shuttle has not been able to establish one. This is the case where nobody should guess, and the only one that needs a person to look before anything else is taken.
  • Failed or not attempted. Nothing was taken. Retrying is safe, and the same nonce can be used.
  • No transaction at all. Nothing reached the gateway. Safe to start again.

An interrupted payment is not a failed payment, and the absence of a receipt is not the absence of a payment.

Related tasks


Did this page help you?