# Stripe

Lightweight, dependency-free, in-memory Stripe REST API fake for testing code that uses the real `stripe` SDK (and the language-agnostic Stripe REST API).

Default port: `4757`

## Quick start

Start the server:

```js
import { StripeServer } from "./services/stripe/src/server.js";

const server = new StripeServer(4757);
await server.start();
// ... run your app/tests ...
await server.stop();
```

Point the real `stripe` client at it. The Stripe SDK reads its host from configuration, so override the base URL to the fake:

```js
import Stripe from "stripe";

const stripe = new Stripe("sk_test_parlel", {
  host: "127.0.0.1",
  port: 4757,
  protocol: "http",
});

const customer = await stripe.customers.create({
  email: "jane@parlel.dev",
  metadata: { plan: "pro" },
});
// customer.id => cus_...
```

Stripe sends requests as `application/x-www-form-urlencoded` (including PHP-style
bracket notation such as `metadata[key]=value`) and receives JSON. The fake
parses both form-encoded and JSON bodies.

## Implemented operations

All `/v1/*` routes require an `Authorization: Bearer sk_test_...` header (any
non-empty bearer/basic token is accepted, matching a local test key). State is
in-memory and ephemeral.

### Customers

- `POST /v1/customers` — create a customer (`200 { id: cus_..., object: "customer", ... }`).
- `GET /v1/customers` — list customers (`{ object: "list", data, has_more, url }`).
- `GET /v1/customers/:id` — retrieve.
- `POST /v1/customers/:id` — update.
- `DELETE /v1/customers/:id` — delete (`{ id, object, deleted: true }`).

### Charges

- `POST /v1/charges` — create a charge (`ch_...`).
- `GET /v1/charges` — list.
- `GET /v1/charges/:id` — retrieve.
- `POST /v1/charges/:id` — update description/metadata.

### Payment intents

- `POST /v1/payment_intents` — create (`pi_...`, returns `client_secret`). `amount` required.
- `GET /v1/payment_intents` — list.
- `GET /v1/payment_intents/:id` — retrieve.
- `POST /v1/payment_intents/:id` — update.
- `POST /v1/payment_intents/:id/confirm` — confirm (`status: succeeded`).
- `POST /v1/payment_intents/:id/cancel` — cancel.

### Refunds

- `POST /v1/refunds` — create a refund (`re_...`); flips the linked charge to refunded.
- `GET /v1/refunds` — list.
- `GET /v1/refunds/:id` — retrieve.

### Products & prices

- `POST /v1/products` — create (`prod_...`). `name` required.
- `GET /v1/products` / `GET /v1/products/:id` — list / retrieve.
- `POST /v1/products/:id` — update. `DELETE /v1/products/:id` — delete.
- `POST /v1/prices` — create (`price_...`).
- `GET /v1/prices` / `GET /v1/prices/:id` — list / retrieve.

### Balance

- `GET /v1/balance` — returns the `balance` object with `available` / `pending`.

### Checkout sessions

- `POST /v1/checkout/sessions` — create (`cs_...`, returns hosted `url`).
- `GET /v1/checkout/sessions` / `GET /v1/checkout/sessions/:id` — list / retrieve.

### Service & inspection operations (parlel extensions, not part of Stripe)

- `GET /` — service metadata.
- `GET /health` — health check (`{ status: "ok" }`).
- `POST /__parlel/reset` — reset all in-memory state.
- `OPTIONS *` — CORS preflight (`204`).

## Access via MCP / preview URL

When run inside a parlel sandbox the service is reachable at its preview URL
(the `STRIPE_BASE_URL` env var, e.g. `http://127.0.0.1:4757`). Point the
`stripe` SDK `host`/`port`/`protocol` (or your `STRIPE_BASE_URL`) at that
address. MCP-driven agents can call any documented endpoint directly; the
`/__parlel/reset` control endpoint clears state between scenarios.

## Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

| Feature | Status |
| --- | --- |
| `customers.*` (create/list/get/update/delete) | ✅ Supported |
| `charges.*` (create/list/get/update) | ✅ Supported |
| `paymentIntents.*` (create/list/get/update/confirm/cancel) | ✅ Supported |
| `refunds.*` (create/list/get) | ✅ Supported |
| `products.*` / `prices.*` | ✅ Supported |
| `balance.retrieve` | ✅ Supported |
| `checkout.sessions.*` (create/list/get) | ✅ Supported |
| Form-encoded (bracket notation) + JSON request parsing | ✅ Supported |
| Deterministic prefixed ids (`cus_`, `ch_`, `pi_`, ...) | ✅ Supported |
| Webhooks / signed events | ⟳ Roadmap — event emission planned |
| Real card processing / 3DS / SCA | ✓ By design — Always succeeds deterministically — no real funds move |
| Subscriptions / invoices / billing schedules | ⟳ Roadmap |
| Idempotency-Key 24h enforcement | ✓ By design — Not enforced |
| Rate limiting (`429`) | ✓ By design — Never throttles — local tests run at full speed, zero cost |
| Bearer-token validity / scope enforcement | ✓ By design — Any non-empty credential is accepted — no real secrets needed |

## Error codes & shapes

Errors use the Stripe envelope:

```json
{ "error": { "type": "invalid_request_error", "message": "...", "code": "resource_missing", "param": "id" } }
```

| Status | When |
| --- | --- |
| `400` | missing required param (e.g. `amount`, `name`), invalid body |
| `401` | no `Authorization` header |
| `404` | unknown resource id or endpoint (`code: resource_missing`) |
| `405` | method not allowed for the path |

## Manifest

See `services/stripe/manifest.json`:

- name: `stripe`, image: `parlel/stripe:1.0`
- port: `4757`, protocol: `http`, healthcheck: `/health`, startup ≈ 100ms
- env: `STRIPE_API_KEY`, `STRIPE_SECRET_KEY`, `STRIPE_BASE_URL`
