← Back to blog

May 20, 2026 · 7 min read

Why I Didn't Just Use Square's Official Plugin

Square publishes a perfectly capable WooCommerce gateway. I shipped a different one anyway — because the kitchen iPad needs orders in a specific shape, and I'd rather not ask restaurant owners to type Square credentials in two places.

In production at Ramen Nagomi and Padi Los Angeles.

Square publishes their own WooCommerce plugin. It’s a perfectly capable general-purpose gateway. The reasonable thing to do — when RestoPick Pro needed Square payments at checkout — would have been to point everyone at Square’s plugin and move on.

I wrote a different one. Three reasons, roughly in order of how much they mattered.

1. The kitchen iPad cares about shape

Pro defines a Square Order builder that formats line items, taxes, tips, and the PICKUP fulfillment in a specific shape — modifiers like “no onions” joined into line_item.note, WC fees mapped to service charges, tips routed through Payment.tip_money instead of the generic bucket. That shape is what makes a WC pickup order look like a walk-in order on the kitchen iPad: same modifier formatting, same tip placement, same fulfillment style.

The official Square plugin uses its own shape. Fine for general e-commerce, subtly different on the POS side. For a restaurant that’s been training kitchen staff on one format for months, “subtly different” is a feature regression nobody asked for.

This gateway reuses Pro’s builder. Every Square Order it creates from a WC sale lands in POS looking identical to walk-in orders. The kitchen sees one format, full stop.

2. Two places to keep credentials is one too many

Pro already has a Square Connection sub-page — that’s where the restaurant enters Application ID, Access Token, and Location ID for sandbox and production. The gateway needs the same values at checkout time.

The lazy option is to make the gateway ask for them again. Two settings pages, two sets of fields, an admin trying to remember which one to update when Square rotates an access token. I’ve watched that drift on other plugins enough times to know how it ends.

So this gateway just reads from Pro’s Square Connection. One place to enter credentials, one place to flip sandbox / production. The gateway’s own settings page only carries what’s actually gateway-specific — the master enable toggle, the title and description shown at checkout, per-method toggles for Card / Google Pay / Apple Pay, and the Apple Pay domain verification button.

3. A smaller surface is easier to keep working

The official gateway supports many use cases I don’t need. A focused gateway that only does the few things a restaurant actually uses ships faster, breaks less, and is something I can confidently maintain alone.

What customers see at checkout

Three methods, each with an admin toggle:

  • A credit card form rendered as a Square-hosted iframe. Card data never touches the WordPress server — the iframe tokenises it locally and the WP side only ever sees an opaque source_id.
  • A Google Pay button in browsers that support it. Silently hides itself elsewhere.
  • An Apple Pay button in Safari on iOS/macOS, once the site’s domain has been verified with Square.

If no method is enabled the gateway hides itself entirely. No greyed-out stub.

The card flow, and what 3-D Secure taught me

Card payments go through two browser steps before the server sees anything sensitive:

  1. card.tokenize() — Square’s iframe turns the card details into a single-use source_id.
  2. payments.verifyBuyer() — Square runs 3-D Secure / SCA against the buyer with the live cart amount, returning a verification_token on success.

Both get written to hidden inputs and posted with the checkout form. The server reads them, builds the Square Order via Pro’s builder, then creates the Payment:

$order_body = Restopick_Pro_Square_POS::build_order_body(
    $order,
    $creds['location_id'],
    $wc_currency,
    true
);

$created = $client->create_order(
    $order_body,
    'restopick-sq-pay-order-' . $order->get_id()
);

The idempotency key is derived from the WooCommerce order id, so a customer who double-clicks Place Order or whose connection drops mid-submit doesn’t get charged twice — Square’s idempotency layer returns the already-created order instead of making a new one. Same idea on the Payment step.

The thing I underestimated was how many failure modes 3-D Secure has on the buyer side. Challenge failed. Challenge cancelled. Browser blocked the popup. Card not enrolled. Each one looks identical at the UI layer if you don’t handle it explicitly. The gateway surfaces a clear notice for each, and writes the Square error payload to the order admin so I can debug from the order page without DMing the merchant for screenshots.

Google Pay, Apple Pay, and the Apple Pay domain dance

Wallets share the same server code path as card. They skip verifyBuyer because the wallet itself authenticates the buyer (FaceID, TouchID, device PIN), so the server just notices which method was used and conditionally omits verification_token.

When the payment settles, Square returns wallet_details describing which wallet was used and the underlying card. The gateway writes both into the WC order note — e.g. **“Apple Pay (Visa **1111)” — so the merchant sees at a glance what was used.

Apple Pay on the web requires Square to register your domain with Apple before the button can render. The flow:

  1. Download apple-developer-merchantid-domain-association from the Square Developer Dashboard.
  2. Drop it into /.well-known/ on your site root.
  3. Click “Verify domain” in the gateway settings.
  4. The plugin posts to Square’s /v2/apple-pay/domains with the current host; Square fetches the well-known file and (if it matches) marks the domain verified.

Sandbox and production are tracked separately. The settings page shows a warning pill if the verified domain doesn’t match the current site host, so a migration or domain change doesn’t silently break Apple Pay weeks later. That pill exists because I ran into all three of those edges during testing — once for a typo, once for a stale sandbox verification carrying mental state into production, once for a CDN rewriting the .well-known response. None of them are interesting on their own. The pill is the part that survives them.

Refunds

Standard WooCommerce admin refund flow. Open the order, click Refund, enter an amount, choose this gateway. The plugin calls Square’s /v2/refunds with the captured payment_id — full and partial refunds both supported. Refund IDs are stored on the order meta so subsequent refunds against the same payment don’t collide on idempotency keys.

The mode (sandbox or production) is stamped on the order at payment time, and the refund flow resolves credentials by that stamp — not whatever mode Pro’s Square Connection currently has selected. So an order paid in sandbox six months ago still refunds against sandbox credentials, even if the connection has been flipped to production since.

No webhook listener (yet). Refund status updates synchronously when the admin triggers one. For restaurants this is fine — refunds are almost always operator-initiated, not customer-initiated. If you need out-of-band reconciliation (chargebacks, automated disputes), Square’s official gateway is the better fit for that workflow.

What I deliberately left out

To keep the surface small:

  • No Cash App Pay. Next on the list.
  • No saved cards / Card-on-File. Restaurant checkout is overwhelmingly guest checkout. I’d rather not own that data path if I don’t have to.
  • No webhooks. See the refund note above.

In one sentence

For a restaurant that already runs Square POS and wants payments and kitchen workflow on one platform, this gateway plus RestoPick Pro produces exactly one Square Order per WooCommerce sale, with the same shape Pro uses everywhere else — card, Google Pay, and Apple Pay all converging on a single, debuggable flow.