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:
POST to connect.squareupsandbox.com/v2/payments answers {"category":"AUTHENTICATION_ERROR","code":"UNAUTHORIZED"}.cnon:card-nonce-declined. Bad CVV? Enter literally 911. Bad postal code? 99999. Expired card? Type 01/40. High-risk payment? Charge exactly 2222 or 3333. Your test suite becomes a lookup table of trivia, and an innocent $22.22 fixture quietly flags MODERATE risk.cnon:card-nonce-ok, cnon:card-nonce-rejected-cvv, cnon:card-nonce-already-used, wnon:cash-app-declinedโฆ โ a fixed list you look up, not outcomes you script.CreatePayment call. Building a checkout screen's loading/error states means standing up that loop first.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.
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.)
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.
/payments/:id/complete endpointSquare'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.
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.
# 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.
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.
| You wanted | Square sandbox | Mockbird |
|---|---|---|
| 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 |
CVV_FAILURE, ADDRESS_VERIFICATION_FAILURE, GENERIC_DECLINE), the sandbox returns the genuine article; Mockbird returns the status you asked for with a generic body (or a custom-route body you define).The clean split: Mockbird while you build โ UI states, declines, retries, webhook handler logic, CI. Square sandbox before you ship โ integration truth. They compose.
amount_money, card_details)? Record a real sandbox session once and replay it โ see mocking third-party APIs.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.