A Braintree sandbox alternative for your dev loop โ€” no merchant ID, no magic amounts

First, credit where due: the Braintree sandbox is the real integration environment โ€” real gateway semantics, the Drop-in UI, 3D Secure flows โ€” and the only place to verify your actual Braintree integration before go-live. Nothing on this page replaces it.

But the everyday dev loop โ€” order 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, Braintree-shaped API on a public HTTPS URL. No account, no keys, no nonce vocabulary โ€” declines are a parameter, not a price.

Testing your checkout flow, decline UX, retry logic, or webhook handler? That's this page. Verifying the real integration โ€” tokenization, Drop-in UI, 3DS, settlement truth? Use the sandbox; see the honest comparison below.

1. A Braintree-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 Braintree-shaped responses specifically.
BASE=https://mockbird.mockbird.workers.dev
P=$(curl -s -X POST $BASE/api/projects -H 'content-type: application/json' \
     -d '{"name":"braintree-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":"transactions",
  "fields":[
    {"name":"status","type":"oneOf","values":["authorized","submitted_for_settlement","settled","processor_declined","gateway_rejected","voided"]},
    {"name":"amount","type":"number"},
    {"name":"currencyIsoCode","type":"oneOf","values":["USD","EUR","GBP"]},
    {"name":"paymentInstrumentType","type":"oneOf","values":["credit_card","paypal_account"]},
    {"name":"customerEmail","type":"email"},
    {"name":"createdAt","type":"pastDate"}
  ],
  "seed":5
}'

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

curl "https://mockbird.mockbird.workers.dev/m/<PID>/transactions?limit=1"
[
  {
    "id": 1,
    "status": "processor_declined",
    "amount": 827,
    "currencyIsoCode": "USD",
    "paymentInstrumentType": "credit_card",
    "customerEmail": "mia.kowalski60@example.com",
    "createdAt": "2025-09-18T08:01:24.661Z"
  }
]

The statuses are Braintree's real transaction vocabulary, so your status badges and state machine exercise real branches. Filters work immediately: ?status=settled, ?amount_gte=500, ?paymentInstrumentType=paypal_account.

2. The sale flow, without the token dance

In the sandbox, a sale is client-token โ†’ nonce โ†’ SDK sale call. Here it's a POST, and the record is really there afterwards (stateful, not a fixture):

# your checkout "authorizes" a transaction
curl -s -X POST $BASE/m/$PID/transactions -H 'content-type: application/json' \
  -d '{"status":"authorized","amount":49.99,"currencyIsoCode":"USD","paymentInstrumentType":"credit_card","customerEmail":"jo@example.com"}'
# โ†’ {"status":"authorized","amount":49.99,โ€ฆ,"id":6}

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

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

Every write is visible to every later read, from any client โ€” browser, CI job, teammate's curl. Walk a transaction through authorized โ†’ submitted_for_settlement โ†’ settled and watch your UI track it.

3. A literal /transactions/:id/submit_for_settlement endpoint

If your client calls a Braintree-style action path, don't change the client โ€” add a custom route with a templated, gateway-shaped body:

curl -s -X POST $BASE/api/projects/$PID/routes -H "x-admin-key: $KEY" \
  -H 'content-type: application/json' -d '{
  "method":"PUT","path":"/transactions/:id/submit_for_settlement","status":200,
  "body":"{\"transaction\":{\"id\":\"{{params.id}}\",\"status\":\"submitted_for_settlement\",\"amount\":\"{{query.amount}}\",\"updatedAt\":\"{{now}}\"}}"
}'

curl -s -X PUT "$BASE/m/$PID/transactions/6/submit_for_settlement?amount=49.99"
{"transaction":{"id":"6","status":"submitted_for_settlement","amount":"49.99","updatedAt":"2026-09-15T15:45:07.487Z"}}

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

4. Declines are a parameter, not a price

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

curl -si -X POST "$BASE/m/$PID/transactions?mock_status=402" \
  -H 'content-type: application/json' \
  -d '{"status":"authorized","amount":10,"currencyIsoCode":"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 $2,034.00 in peace. Full recipes in testing loading & error states.

5. Exact retry choreography: declined, declined, settled

# first attempt 402, second 402, third succeeds โ€” exactly
curl -s -o /dev/null -w '%{http_code}\n' -X POST \
  "$BASE/m/$PID/transactions?mock_seq=402,402,201&mock_seq_key=bt-retry-1" \
  -H 'content-type: application/json' \
  -d '{"status":"authorized","amount":20,"currencyIsoCode":"EUR"}'
# 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 charging a magic amount twice and then changing the fixture's price.

6. Signed webhooks โ€” no gateway 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/braintree"}'
# โ†’ {"ok":true,"webhook":{"secret":"whsec_bebb10c56a11โ€ฆ","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-submitted_for_settlement from section 2, which is exactly the "payment finished" event your handler cares about. Verify snippet and delivery log in sending test webhooks.

Braintree-sandbox concern โ†’ Mockbird

You wantedBraintree sandboxMockbird
First request without an accountโœ— signup + merchant ID + public/private keys + SDKโœ” one anonymous curl
Trigger a decline explicitlyโœ— charge a magic amount (2000.00โ€“2999.99)โœ” ?mock_status=402, any amount
Simulate any payment method outcomeโ–ณ enumerated fake-*-nonce listโœ” you define the record and the outcome
Build UI before the token loop existsโœ— client token โ†’ nonce โ†’ sale requiredโœ” 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. Braintree 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.