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:
"Authentication credentials are missing. Authorization header is required". Before your first fake charge you need a sandbox signup and three credentials wired into an SDK.2000.00 and 2999.99 โ the amount becomes the processor response code. 3000.00โ3000.99 means Failed; 5001.00 means Gateway Rejected. Your test suite ends up encoding a price list of failure modes, and an innocent-looking $2,034.00 test fixture declines for reasons nobody remembers.fake-valid-nonce, fake-processor-declined-visa-nonce, and friends โ a fixed vocabulary you look up, not outcomes you script.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.
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.
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.
/transactions/:id/submit_for_settlement endpointIf 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.
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.
# 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.
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.
| You wanted | Braintree sandbox | Mockbird |
|---|---|---|
| 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 |
The clean split: Mockbird while you build โ UI states, declines, retries, webhook handler logic, CI. Braintree sandbox before you ship โ integration truth. They compose.
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.