How to identify and handle 'CANCELLED' payments?

I will be happy to provide webhooks showing examples of this problem.
I just became aware of the ‘CANCELLED’ status on payments. My wife owns a small yoga studio: I handle Square Data processing – she looks at the square dashboard.

The studio sells ‘E-Cards’ for class attendance. My home-built website creates an internal customer id, and then saves the Square ID for the customer. Customer then enters a credit card, for payment for an E-card (20). When the payment is approved, we update the customer records the payment id, and with an E-Card PIN so they can enter the class.

Today a new first-time customer followed this process and attended a class.

My wife wanted more information from the Square Dashboard, and there were no transactions shown. I’m first line tech support, so I looked at database records, including payment webhooks that I store.

I found webhookks for customer creation, card creation, payment creation.

I also capture payment.updated webhooks, but my experience is that the ‘updates’ are sometimes not visible at all – I requested info, and was told that some updates are internal to square, and don’t show on webhooks. So I have mostly been ignoring payment updates.

This time I checked those updates.
After the payment.created webhook there were 2 additional webhooks version 2 and 3, related to the payment id.

Version 2 arrived 2 seconds after version 1: Only signifcant change to version 2 was this:

[card_payment_timeline] => Array                        
    (                                                   
        [authorized_at] => 2025-07-24T00:22:30.576Z     
        **[voided_at] => 2025-07-24T00:22:32.330Z**         
    )   

Version 3 arrived 12 seconds later: Only significant differences between versions 2 and 3:
[payment][card_detatils][status] => changed from AUTHORIZED tp VOIDED
and
[payment][status] => changed from AUTHORIZED to CANCELLED

There was no record of any of the payment transaction, or these payment versions on the Square Dashboard.

My wife spoke to the student. She told us that she looked at her credit card statement and found the charge (same time as transaction 1), then found a second entry “REFUNDED”. Student had NOT requested a refund.

I logged into my credit card account, and it seems like I got charged on the 27th at 9:41 pm and it got refunded at 9:42 pm.

So the student had assumed she’d paid, and our system did too.

I need to understand what happened, and what to do about it?
I don’t know how to keep watching webhooks for cancellations, when we’ve already given the student the class they ‘paid’ for?

I have now found similar instances in my webhook database, and can provide those in detail if it helps.

Thanks.

1 Like

:waving_hand: I’m Square Dev AI, here to help answer your question!
Based on the information you’ve provided, the following documentation will help answer your question:

Additional Documentation

Use Customer Webhooks
Gift Card Webhooks
Pay or Refund Invoices

If this documentation helped you find a solution, please check the box indicating that this reply solves the problem.
Otherwise one of our Developer Advocates will provide additional assistance shortly.

What is the payment_id for the payment in question? :slight_smile:

vpPb6zbPsEq7AbuVDMvqnLwHVlIZY

Looks like that payment failed due to an incorrect order_id. :slight_smile:

{
  "errors": [
    {
      "code": "NOT_FOUND",
      "detail": "Order not found for id ",
      "category": "INVALID_REQUEST_ERROR"
    }
  ],
  "payment": {
    "id": "vpPb6zbPsEq7AbuVDMvqnLwHVlIZY",
    "created_at": "2025-07-24T00:22:29.908Z",
    "updated_at": "2025-07-24T00:22:30.861Z",
    "amount_money": {
      "amount": 2000,
      "currency": "USD"
    },
    "status": "CANCELED",
    "delay_duration": "PT168H",
    "source_type": "CARD",
    "card_details": {
      "status": "VOIDED",
      "card": {
        "card_brand": "MASTERCARD",
        "last_4": "2479",
        "exp_month": 3,
        "exp_year": 2027,
        "fingerprint": "sq-1-7PoUyLMFbDrwEhNWxQ5E1WNr8PrvS7mrToVgwO9ST-oSQjfa_NU24Kg0zKtp8hankQ",
        "card_type": "DEBIT",
        "prepaid_type": "NOT_PREPAID",
        "bin": "527519",
        "payment_account_reference": "50011PFDH8RZ5ZPMHUD7U0ANIS4E1"
      },
      "entry_method": "ON_FILE",
      "cvv_status": "CVV_NOT_CHECKED",
      "avs_status": "AVS_ACCEPTED",
      "auth_result_code": "102122",
      "statement_description": "SQ *WEAVERVILLE YOGA INC",
      "card_payment_timeline": {
        "authorized_at": "2025-07-24T00:22:30.576Z",
        "voided_at": "2025-07-24T00:22:30.861Z"
      }
    },
    "location_id": "91F1VEX85751A",
    "order_id": "",
    "risk_evaluation": {
      "created_at": "2025-07-24T00:22:30.768Z",
      "risk_level": "NORMAL"
    },
    "customer_id": "ZNB65WXZSX9XX6Z2SZJNKQHX0G",
    "total_money": {
      "amount": 2000,
      "currency": "USD"
    },
    "approved_money": {
      "amount": 2000,
      "currency": "USD"
    },
    "delay_action": "CANCEL",
    "delayed_until": "2025-07-31T00:22:29.908Z",
    "application_details": {
      "square_product": "ECOMMERCE_API",
      "application_id": "sq0idp-XX6UWAX013YgL8UN-wNg8w"
    },
    "version_token": "cK3pcuHrXOvcv4bZIri6NbOeHSgsw4VWvJmRLuTLD755o"
  }
}

:slight_smile:

Our E-card payments are simply payments to the studio: they do not have related orders.
Order ID is not a required field in the Create Payment API.

If a missing order ID was a consistent cause of failure, many hundred payments would be failing.

Also your answer suggests the VOID was issued by Square? Not by cutomer or Card Company?

I have just gone through the web hooks looking for ‘VOID’ and all these E-Card payments were voided. I just discovered that we lost thousands of dollars, and Square never sent specific notification of payment failure.

Some notfication, at least on the Square dashboard would have been helpful.

Making Order_ID a required field on the Create_Payment API should be done.

This is devestating information.

$17,960 in lost revenue. I don’t know how to recover.

I accept that it was my coding error that caused the multiple instances of payment failures - I had included a NULL order_id in the payment curl.

I had gotten used to bad curl results saying ‘failed’ and so I check for ‘failed’ in the result array (and deal with failed results with an error handler). But in this case the results include words like ‘CANCEL’, ‘VOID’, etc.. so I didn’t treat the result as an error.

PLEASE NOTE that even if i had found the ‘error’ result for the curl, THERE WAS NOTHING I COULD DO to keep it from being submitted. The error only shows up when the curl is executed, and a payment has been intitated.

Also, the events that follow these ‘error’ payment curls are utterly confusing. Now that I know what happens, it’s just nuts. I just don’t get what Square is doing.

Despite noting the error, Square submits the (so-called error-filled) payment record to the bank. The bank accepts the record, and charges the card.

Following this, however, Square on its own sends the bank a new command to refund the charge it has just accepted. Even though the BANK ACCEPTED THE PAYMENT, Square, on its own, makes additional changes: Square, on its own, ISSUES A REFUND.

I have been looking at the process more closely now, Although I had been checking for errors in the ‘payment.created’ webhook, for these error-identified curls, the payment status was ‘APPROVED’

I have since looked deeper, and found that every payment, with or without errors, has 3 webhook entries.
Webhook 1 – payment.created – This status always ‘APPROVED’
Webhook 2 – payment.updated - This also always says status ‘APPROVED’
but if there’s error, a new entry appears is [payment][card_payment_timeline] =>
[voided_at] => 2025-07-29T12:17:52.371Z
The payment status ONLY CHANGES IN
Webhook 3 - payment.updated which now says [status] => CANCELED (only when Square identified the curl as having errors)

I can find no documentation for a payment canceled status. But my bank records show that a refund for the payment amount was initiated by Square

So I’m stupid not to have done more extensive error checking of my payment curl. But this doesn’t explain why

  1. Square noted the error
  2. Despite the error, Square submitted the payment, with status ‘approved’
  3. Updated the payment with an obscure new entry to [payment]=[card_payment_timeline] (‘voided’) - But did not also then change the payment status (status still shows ‘APPROVED’)
  4. Sent a refund commend to the bank – with no findable transaction record anywhere
  5. Failed to show ANY transaction records on the Square dashboard – although both a deposit transaction and a refund transaction were sent to the bank.

I’m ready to own responsibilty for creating a curl that Square said had errors
But I believe that Square shares responsibility for this mess:

  1. Submitting the curl anyway – (which the bank thought was just fine, although Square found an ‘error’.)
  2. Issuing an unnecessary refund (at least the bank didn’t think a refund was needed).
  3. Failing to show any of these transactions on the Square Dashboard, so the owner was not aware of any problems.

In fact if the confused student hadn’t questioned her bank statement, and reported this mess to the owner, who then asked, we would never have discovered a problem that has cost us thousands of dollars. So i do believe that Square owns a lot for responsibilty for this disaster.

Thank you for walking me through exactly what you saw in your logs and on your bank statement. I’m really sorry for the confusion and inconvenience you’ve been through. Let me try to pull back the curtain on exactly what’s happening inside Square so you can see why you saw an approved‐then‐voided payment, a refund on your bank statement, and very little detail back on the Dashboard.

  1. Your request and the “non-blocking” error
    • You submitted a CreatePayment call with order_id set explicitly to null.
    • Square’s API schema treats that as an unsupported attribute, so we return an error in the response body under the “errors” array (e.g. category=INVALID_ATTRIBUTE, code=UNSUPPORTED_ATTRIBUTE).
    • HOWEVER, because order_id is not required to authorize and capture a card, we treat that particular error as “non-blocking.” We still proceed to call the card networks, authorize the card. In short: you got an API error about order_id, but Square we still initially authorized the payment.
  2. The automatic “fail-safe” refund/void
    Once our system spots that a payment was successfully processed despite having a non-blocking error in its payload, we automatically void the authorization since the payment was just authorized and not fully captured.
  3. Why you saw three webhooks
    • webhook #1 – payment.created
    – status: APPROVED
    – This is Square saying “we accepted the authorization (and since auto_capture=true, we also captured).”
    • webhook #2 – payment.updated
    – status: still APPROVED
    – A timeline event appears under card_payment_timeline showing voided_at. At that moment we’ve told the bank to release or refund the money.
    • webhook #3 – payment.updated
    – status: CANCELED (or REFUNDED)
    – The payment object flips to CANCELED once the void has been successfully queued in our system.
  4. Why you don’t see a matching void in the Dashboard’s Orders view
    These automatic “fail-safe” voids aren’t tied to an Order in your Orders list—because Square never fully completed the payment. Only fully completed payments will show in the Dashboard.

Again, I’m sorry for the runaway charges and confusing webhooks. I hope this breakdown of our “non-blocking error → auto-capture → fail-safe void/refund” workflow clears up what you witnessed in your logs, on your bank statement, and on the Dashboard. Please let me know if you have any follow-up questions. :slight_smile:

Thanks for the full process information Bryan. I pictured something like what your show.
However, I think its a big problem on your end that

  1. The payment with a non-blocking error is only identified if the payment is submitted.
  2. If the curl has a non-blocking error, there is apparently no way create an error handler that blocks submission – the error appears to only be created when the curl is set in process
  3. If the curl is identified with non-blocking error, the curl is still submitted to the bank.

You say
Square never fully completed the payment. Only fully completed payments will show in the Dashboard.
But from the bank’s point of view, both the payment and the void are fully completed. This causes buyer confusion, and buyer mistrust of the business. There is an unfortunate disconnect between the Dashboard records seen by the Dashboard user, and the bank records seen on the buyer’s bank statement. (What the heck is this charge?? And why the immediate refund? What you guys up to?? etc. [Owner checks the Dashboard and has no clue.])

As to the square dashboard tranasctions page, ‘completed’ transactions only are now shown. But some button or menu option showing ‘error’ or similar that would show ‘failed’ transactions would be a big help.

The Quick and Dirty Solution® (my favorite brand) would be to turn an ‘order id not found’ into a BLOCKING error. Alternately to require order_id in the curl explicitly much as source_id is required (maybe both suggestions are effectively equivalent).

But for Pete’s sake – there needs to be some automatic or manual way to block the curl if it’s going to be voided automatically!!

When a payment is AUTHORIZED it will be in a pending state. If the authorization falls off or is voided then the AUTHORIZED funds will return to the customers balance. That is what your seeing on the bank statement. If the payment went into a COMPLETED state then it will show in the Square Dashboard and will show as a completed payment in the customers bank statement.

As for the other behavior of the Payments API we’re constantly working to improve our features based on feedback like this, so I’ll be sure to share your request to the API product team. :slight_smile: