Skip to content

Stripe

The second hand-authored validation: 24 endpoints across 5 resources (customer, payment_method, payment_intent, refund, transfer) and 2 actors (platform, connected).

It breaks assumptions a JSON API doesn’t:

  • Form-encoded, not JSON, with bracket notation for nested fields
  • Mandatory idempotency keys on every mutating request
  • Multi-tenancy through a header — same credential, different acting account
  • HTTP Basic with an empty password — the secret key is the username

Stripe accepts application/x-www-form-urlencoded. So do Twilio and essentially every OAuth 2 token endpoint. This was Finding 5, and it shipped as body_form:

create:
method: POST
path: /v1/customers
actor: platform
body_form:
email: "{{$.faker.email}}"
name: "Reqloom Test"
"metadata[source]": "reqloom"
expect_status: 200
extract:
customer_id: $.id

Bracket keys are preserved verbatim — quote them in YAML and they arrive as metadata[source]=reqloom, which is what Stripe expects for nested objects.

body_form also picks the encoding for you: it’s application/x-www-form-urlencoded unless a value starts with @, in which case it becomes multipart. See file uploads.

Stripe requires one on every mutating call. This is the single clearest win over an HTTP client:

headers:
Idempotency-Key: "{{$.uuid}}"

One line, per operation, generated fresh per request. In Postman this needs a pre-request script on every endpoint.

Stripe Connect acts on behalf of a connected account by adding a Stripe-Account header. The credential is identical; the acting identity is not — so it’s two actors:

actors/platform.yaml
name: platform
description: The platform account itself
auth:
strategy: basic
username: "{{secret.STRIPE_SECRET_KEY}}"
password: ""
actors/connected.yaml
name: connected
description: Acting on behalf of a connected account
auth:
strategy: basic
username: "{{secret.STRIPE_SECRET_KEY}}"
password: ""
inject:
headers:
Stripe-Account: "{{env.connected_account_id}}"

Switching identity is then one word on an operation:

create_on_behalf:
method: POST
path: /v1/charges
actor: connected # ← the only difference

The validation called this out as the abstraction Stripe Connect needs and that no existing tool offers. Modelling it as two actors keeps every operation honest about whose behalf it acts on. See one actor per identity.

Stripe puts the secret key in the username and leaves the password blank. That requires base64(key + ":").

Finding 4 asked for a !basic YAML transformer. That wasn’t implemented — what shipped is the basic auth strategy, which computes the header for you:

auth:
strategy: basic
username: "{{secret.STRIPE_SECRET_KEY}}"
password: ""

No inject: needed; the Authorization: Basic … header is added automatically. If you ever need the encoding by hand, {{$.base64.encode('key:')}} is available.

customer.create → payment_method.create → payment_method.attach
→ payment_intent.create → payment_intent.confirm
→ refund.create

Five levels, which the validation noted is normal in payments and composed without fanfare. Stripe’s test tokens (tok_visa) plus a test API key meant the whole chain ran end-to-end against real Stripe with no mocks — worth remembering when you’re deciding whether to build a mock server.

  • expand[] eager-loading — an ordinary query param
  • API version pinning via Stripe-Version — an ordinary injected header
  • Webhook signature validation — out of test scope, and the legitimate case for a pre_request HMAC hook
Stripe patternWhat to use
Form-encoded bodybody_form:
Nested form fieldQuoted bracket key — "metadata[source]"
Idempotency keyIdempotency-Key: "{{$.uuid}}"
Secret key as Basic usernamestrategy: basic with password: ""
Acting on behalf of another accountA second actor with an extra inject: header
API version pinningAn injected header
Repeated query keysquery_params:, not body_form: