A Square sandbox alternative for your dev loop โ€” no access token, no magic test values

First, credit where due: the Square sandbox is the real integration environment โ€” real Payments API semantics, the Web Payments SDK, SCA challenge flows, even a risk-evaluation simulator โ€” and the only place to verify your actual Square integration before go-live. Nothing on this page replaces it.

But the everyday dev loop โ€” checkout screens, decline UX, retry logic, webhook handlers โ€” pays a sandbox tax that has nothing to do with what you're testing:

This guide builds the mock version in one paste: a hosted, stateful, Square-shaped API on a public HTTPS URL. No account, no token, no nonce vocabulary โ€” declines are a parameter, not a magic number.

Testing your checkout flow, decline UX, retry logic, or webhook handler? That's this page. Verifying the real integration โ€” tokenization, SCA challenges, risk evaluation, Cash App / ACH flows? Use the sandbox; see the honest comparison below.

1. A Square-shaped mock in one paste

New โ€” skip the paste: one click (or curl -X POST https://mockbird.mockbird.workers.dev/api/projects -H 'content-type: application/json' -d '{"preset":"payments"}') creates a ready payments sandbox: charges/refunds/subscriptions/customers CRUD plus a Stripe-shaped POST /v1/payment_intents → /v1/payment_intents/:id/confirm flow and /v1/balance. The paste below builds a custom-shaped mock instead โ€” use it when you want Square-shaped responses specifically.
BASE=https://mockbird.mockbird.workers.dev
P=$(curl -s -X POST $BASE/api/projects -H 'content-type: application/json' \
     -d '{"name":"square-mock","blank":true}')
PID=$(echo $P | sed 's/.*"id": *"\([^"]*\)".*/\1/')
KEY=$(echo $P | sed 's/.*"adminKey": *"\([^"]*\)".*/\1/')

curl -s -X POST $BASE/api/projects/$PID/resources -H "x-admin-key: $KEY" \
  -H 'content-type: application/json' -d '{
  "name":"payments",
  "fields":[
    {"name":"status","type":"oneOf","values":["APPROVED","PENDING","COMPLETED","CANCELED","FAILED"]},
    {"name":"amount","type":"number"},
    {"name":"currency","type":"oneOf","values":["USD","CAD","GBP"]},
    {"name":"sourceType","type":"oneOf","values":["CARD","CASH_APP","BANK_ACCOUNT","WALLET"]},
    {"name":"buyerEmail","type":"email"},
    {"name":"createdAt","type":"pastDate"}
  ],
  "seed":5
}'

You now have GET/POST/PATCH/DELETE /m/<PID>/payments with 5 seeded, Square-shaped records:

curl "https://mockbird.mockbird.workers.dev/m/<PID>/payments?limit=1"
[
  {
    "id": 1,
    "status": "PENDING",
    "amount": 302,
    "currency": "GBP",
    "sourceType": "CASH_APP",
    "buyerEmail": "amelia.martinez83@example.com",
    "createdAt": "2026-03-21T17:17:51.650Z"
  }
]

The statuses are the Payment object's real vocabulary (APPROVED, PENDING, COMPLETED, CANCELED, FAILED), so your status badges and state machine exercise real branches. Filters work immediately: ?status=COMPLETED, ?amount_gte=500, ?sourceType=CASH_APP. (Amounts here are plain numbers โ€” treat them as cents like Square's amount_money.amount if you like; it's your schema.)

2. Delayed capture, without the token dance

In the sandbox, a payment is SDK โ†’ test card โ†’ payment token โ†’ CreatePayment. Here it's a POST, and the record is really there afterwards (stateful, not a fixture):

# your checkout "authorizes" a payment (autocomplete=false style)
curl -s -X POST $BASE/m/$PID/payments -H 'content-type: application/json' \
  -d '{"status":"APPROVED","amount":4999,"currency":"USD","sourceType":"CARD","buyerEmail":"jo@example.com"}'
# โ†’ {"status":"APPROVED","amount":4999,โ€ฆ,"id":6}

# your UI polls โ€” the write persisted
curl -s $BASE/m/$PID/payments/6              # status: "APPROVED"

# the test driver captures it
curl -s -X PATCH $BASE/m/$PID/payments/6 -H 'content-type: application/json' \
  -d '{"status":"COMPLETED"}'

Every write is visible to every later read, from any client โ€” browser, CI job, teammate's curl. Walk a payment through APPROVED โ†’ COMPLETED (or CANCELED) and watch your UI track it.

3. A literal /payments/:id/complete endpoint

Square's delayed-capture flow completes a payment with POST /v2/payments/{id}/complete. If your client calls an action path like that, don't change the client โ€” add a custom route with a templated, Square-shaped body:

curl -s -X POST $BASE/api/projects/$PID/routes -H "x-admin-key: $KEY" \
  -H 'content-type: application/json' -d '{
  "method":"POST","path":"/payments/:id/complete","status":200,
  "body":"{\"payment\":{\"id\":\"{{params.id}}\",\"status\":\"COMPLETED\",\"updated_at\":\"{{now}}\"}}"
}'

curl -s -X POST "$BASE/m/$PID/payments/6/complete"
{"payment":{"id":"6","status":"COMPLETED","updated_at":"2026-09-15T16:42:12.223Z"}}

Custom routes take precedence over the generated CRUD routes, so this coexists with the payments resource above โ€” {{params.id}} and {{now}} are filled per request.

4. Declines are a parameter, not a magic value

No nonce vocabulary, no CVV trivia โ€” ask for the failure you want, when you want it:

curl -si -X POST "$BASE/m/$PID/payments?mock_status=402" \
  -H 'content-type: application/json' \
  -d '{"status":"APPROVED","amount":1000,"currency":"USD"}'
# HTTP/2 402
# {"error": "simulated 402 error (mock_status)"}
# and no phantom record is created โ€” the write is not applied

Any status 400โ€“599 works: 429 for rate-limit UX, 500/503 for gateway-down banners, and mock_delay=4000 composes for slow-failure paths. Your fixtures can charge $22.22 in peace. Full recipes in testing loading & error states.

5. Exact retry choreography: declined, declined, completed

# first attempt 402, second 402, third succeeds โ€” exactly
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
  "$BASE/m/$PID/payments?mock_seq=402,402,201&mock_seq_key=sq-retry-1" \
  -H 'content-type: application/json' \
  -d '{"status":"APPROVED","amount":2000,"currency":"USD"}'
# run it 3x: 402, 402, 201

Each mock_seq_key tracks its own position, so parallel test workers don't trample each other's sequences. In the sandbox the closest equivalent is juggling cnon:card-nonce-declined and cnon:card-nonce-ok between runs by editing the fixture.

6. Signed webhooks โ€” no Square Dashboard configuration, no tunnel

curl -s -X PUT $BASE/api/projects/$PID/webhook -H "x-admin-key: $KEY" \
  -H 'content-type: application/json' -d '{"url":"https://your-app.example.com/hooks/square"}'
# โ†’ {"ok":true,"webhook":{"secret":"whsec_816e037ae739โ€ฆ","events":["created","updated","deleted"]}}

Every create/update/delete fires a signed POST (X-Mockbird-Signature: sha256=โ€ฆ, HMAC-SHA256 of the raw body) so your verification code path actually runs โ€” including the PATCH-to-COMPLETED from section 2, which is exactly the payment.updated moment your handler cares about. Verify snippet and delivery log in sending test webhooks.

Square-sandbox concern โ†’ Mockbird

You wantedSquare sandboxMockbird
First request without an accountโœ— developer account + application + access tokenโœ” one anonymous curl
Trigger a decline explicitlyโœ— magic source ID cnon:card-nonce-declinedโœ” ?mock_status=402, any payload
Trigger a bad CVV / postal / expiryโœ— magic values 911 / 99999 / 01/40โœ” the status and body you asked for
Simulate any payment outcomeโ–ณ enumerated cnon:/wnon: nonce listโœ” you define the record and the outcome
Build UI before the token loop existsโœ— SDK โ†’ test card โ†’ token โ†’ CreatePaymentโœ” plain HTTP from the first minute
Scripted outcome sequencesโœ—โœ” ?mock_seq=402,402,201
Deterministic latencyโœ— real shared environmentโœ” ?mock_delay=, jitter, chaos
Reset to a known dataset per testโœ—โœ” snapshots
Real gateway integration truthโœ” that's its jobโœ— your own shapes

Honest comparison โ€” where the sandbox wins

The clean split: Mockbird while you build โ€” UI states, declines, retries, webhook handler logic, CI. Square sandbox before you ship โ€” integration truth. They compose.

Notes & limits

Create yours

The paste block in section 1 is the whole setup โ€” or open the dashboard and click it together. The same project also serves GraphQL, exports OpenAPI + TypeScript types, and ejects to db.json anytime โ€” no lock-in.