Run billing code against a local Stripe: the API subset billing uses, test clocks, signed webhooks and a control API.
The Stripe fake answers the part of the Stripe API that Fonoster’s billing uses, so billing code and
its tests run with no Stripe account and no network. It lives in mods/stripe-fake.Its answers follow Stripe’s own OpenAPI schema at a pinned version (API 2026-09-30.endive,
stripe npm 23), and its tests check every response against that schema. It covers customers and
payment methods, products and prices, billing meters and meter events, credit grants,
subscriptions and invoices under test clocks, payment intents and refunds, Customer Portal
sessions, events, and signed
webhooks.
The fake is for development and tests only. It keeps everything in memory and accepts any
sk_test_ key.
Open http://localhost:12111 for the viewer: what Stripe would see, updated live as your code or
tests call the fake.
Customers: the credit balance (1 credit = $0.01), plan and top-up credits, meter totals and
recent webhooks.
Meters: each meter and its recent events, accepted or rejected.
Credit grants, Invoices (with line items) and Webhooks (each delivery’s attempts,
payload and a Replay button).
The top bar moves the test clock a month ahead and switches between light and dark. The sidebar
can make the next payment fail or reset everything.To work on the viewer itself, run a fake on 12111 and then:
npm run dev:viewer -w mods/stripe-fake
It serves on 12112 and sends API calls to the fake (STRIPE_FAKE_URL to change it).
Requests need a sk_test_ or rk_test_ key. A Stripe-Version header, when sent, must be the
pinned version. Unknown parameters and paths are refused with Stripe’s error shape, and
Idempotency-Key replays work as in Stripe.Stripe’s test payment methods work: pm_card_visa, pm_card_mastercard,
pm_card_chargeDeclined, pm_card_chargeDeclinedInsufficientFunds and
pm_card_authenticationRequired.
Create a test clock and attach customers to it, as in Stripe. Advancing the clock runs the
billing cycle: renewals, invoices finalized after the grace period (one hour by default), credit
grants applied to metered lines, payment, and retries after 3, 5 and 7 days.
Create endpoints with stripe.webhookEndpoints.create, or at start with STRIPE_FAKE_WEBHOOKS:
a URL and its signing secret joined by |, with commas between endpoints.
Deliveries are
signed like Stripe’s, so stripe.webhooks.constructEvent verifies them. A delivery that doesn’t
get a 2xx is retried with backoff. Meter event errors arrive as thin v2.core.event events; read
the full event at GET /v2/core/events/{id}.
The tests drive the fake through the official Stripe SDK. To refresh the schema after moving to a
new Stripe API version, change OPENAPI_RELEASE in scripts/extract-openapi.ts and run
npm run extract:openapi -w mods/stripe-fake.
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"Fonoster","description":"The AI that answers the phone. Build, test, and run voice agents on real phone lines.","colors":{"primary":"#008751","light":"#39E19E","dark":"#008751"},"logo":{"light":"https://mintcdn.com/fonoster/GsxtawNPYOQvohG4/logo/light.svg?fit=max\u0026auto=format\u0026n=GsxtawNPYOQvohG4\u0026q=85\u0026s=4b95dca8d9d982d3851611f31fb2f3f6","dark":"https://mintcdn.com/fonoster/GsxtawNPYOQvohG4/logo/dark.svg?fit=max\u0026auto=format\u0026n=GsxtawNPYOQvohG4\u0026q=85\u0026s=b8522165ef519d21c1d9e448539b0b85","href":"https://fonoster.com"},"favicon":"https://mintcdn.com/fonoster/GsxtawNPYOQvohG4/favicon.svg?fit=max\u0026auto=format\u0026n=GsxtawNPYOQvohG4\u0026q=85\u0026s=9c788ad924519260668c5c7faad21fff","appearance":{"default":"dark"},"background":{"color":{"light":"#FFFFFF","dark":"#181818"}},"navbar":{"links":[{"href":"https://fonoster.com","label":"Website"},{"href":"https://form.typeform.com/to/DVAwugzh","label":"Support"}],"primary":{"type":"button","label":"Dashboard","href":"https://app.fonoster.ai"}},"footer":{"socials":{"x":"https://x.com/fonoster","github":"https://github.com/fonoster","linkedin":"https://linkedin.com/company/fonoster"}},"fonts":{"heading":{"family":"Poppins","weight":600},"body":{"family":"Poppins"}},"styling":{"codeblocks":"system","latex":false},"contextual":{"options":["copy","view","chatgpt","claude","perplexity","mcp","cursor","vscode"],"display":"header"},"navigation":{"global":{"anchors":[{"anchor":"GitHub","icon":"github","href":"https://github.com/fonoster/fonoster"},{"anchor":"Community","icon":"discord","href":"https://discord.gg/4QWgSz4hTC"}]},"pages":[]}},"deployment":{"deploymentMetadataValue":{"subdomain":"fonoster","hasAudioNarration":false,"actualSubdomain":"fonoster","customDomains":["docs.fonoster.com"],"gitSource":{"type":"github","owner":"fonoster","repo":"fonoster","deployBranch":"main","contentDirectory":"docs-site","isPrivate":false,"mountPath":"","label":"","_id":"6adca3135051fd671a993432"},"chroma":{"liveDeploymentHistoryId":"6a21e67cf325d2bf100606ba","collectionId":"e5dbd598-2128-4865-ba86-14d89221de7c","schemaVersion":"v2"},"feedback":{"thumbs":true,"edits":true,"issues":true},"entitlements":{},"buildId":"6adca3135051fd671a993438","clientVersion":"0.0.3964","searchFilterCounts":[{"language":"default","tag":"Docs","count":47},{"language":"default","tag":"Documentation","count":23},{"language":"default","tag":"Concepts","count":10},{"language":"default","tag":"Telephony","count":10},{"language":"default","tag":"SDK","count":7},{"language":"default","tag":"Self-hosting","count":7},{"language":"default","tag":"Autopilot","count":6},{"language":"default","tag":"Start","count":5},{"language":"default","tag":"Get started","count":4},{"language":"default","tag":"Guides","count":4}]},"assistantConfig":{},"hasChatPermissions":false,"hasWebMcpPermissions":false,"toggles":[{"name":"kill-isr","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-mintlify-slack","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-v7-file-tree","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-integrations-settings","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-pr-grouping","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-prefix","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"analytics-v7","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"user-flows-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"no-publish-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-onboarding","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"llms-txt-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"pause-universal-search-reconcile","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving-live","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"stripTemperatureMiddleware","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"per-site-search-delta","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"support-platform-pylon-zendesk","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"instant-rollback","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications-server","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-private-page-search","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-fable","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fixed-ai-action-pricing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automation-resolve-merge-conflict","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"scoped-publishing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"mintql","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"page-level-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-version-history-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-nav-generations","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-page-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workspace-projects","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"assistant-v5","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"byok-sep-25","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fork-pr-previews","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"revalidation-jwt","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"vite-origin","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-agent-model-picker","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-updated-9-15","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"hobby-abuse-scan","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"dashboard-inbox","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false}],"isAtlas":false,"googleFontsInlined":true},"customCss":[{"filePath":"style.css","content":"/* cyrillic-ext */\n@font-face {\n font-family: 'Roboto Mono';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(/_mintlify/assets/fonts/google/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2) format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Roboto Mono';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(/_mintlify/assets/fonts/google/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2) format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek */\n@font-face {\n font-family: 'Roboto Mono';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(/_mintlify/assets/fonts/google/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2) format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Roboto Mono';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(/_mintlify/assets/fonts/google/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2) format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Roboto Mono';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(/_mintlify/assets/fonts/google/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2) format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Roboto Mono';\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(/_mintlify/assets/fonts/google/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2) format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n/* cyrillic-ext */\n@font-face {\n font-family: 'Roboto Mono';\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(/_mintlify/assets/fonts/google/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhGq3-OXg.woff2) format('woff2');\n unicode-range: U+0460-052F, U+1C80-1C8A, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;\n}\n/* cyrillic */\n@font-face {\n font-family: 'Roboto Mono';\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(/_mintlify/assets/fonts/google/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhPq3-OXg.woff2) format('woff2');\n unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;\n}\n/* greek */\n@font-face {\n font-family: 'Roboto Mono';\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(/_mintlify/assets/fonts/google/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhIq3-OXg.woff2) format('woff2');\n unicode-range: U+0370-0377, U+037A-037F, U+0384-038A, U+038C, U+038E-03A1, U+03A3-03FF;\n}\n/* vietnamese */\n@font-face {\n font-family: 'Roboto Mono';\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(/_mintlify/assets/fonts/google/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhEq3-OXg.woff2) format('woff2');\n unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;\n}\n/* latin-ext */\n@font-face {\n font-family: 'Roboto Mono';\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(/_mintlify/assets/fonts/google/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhFq3-OXg.woff2) format('woff2');\n unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C4, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: 'Roboto Mono';\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(/_mintlify/assets/fonts/google/s/robotomono/v31/L0x5DF4xlVMF-BfR8bXMIjhLq38.woff2) format('woff2');\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;\n}\n:root{--fn-accent:#39e19e;--fn-on-accent:#011900;--fn-accent-text:#008751;--fn-border:#e8e8e8;--fn-hl-border:rgba(0, 0, 0, 0.22);--fn-surface:#f4f4f4;--fn-text-1:#252525;--fn-text-2:#555555;--fn-text-3:#6b6b6b;--fn-mono:\"Roboto Mono\", ui-monospace, monospace}:root.dark{--fn-accent-text:#39e19e;--fn-border:#333333;--fn-hl-border:rgba(255, 255, 255, 0.32);--fn-surface:#202020;--fn-text-1:#ffffff;--fn-text-2:#e8e8e8;--fn-text-3:#c2c2c2}#content-area h1,#header h1{font-weight:600;letter-spacing:-.031em}#content-area h2{font-weight:600;letter-spacing:-.027em}#content-area h3{font-weight:600;letter-spacing:-.025em}#content-area li,#content-area p{color:var(--fn-text-2)}.eyebrow{font-family:var(--fn-mono);font-weight:500;font-size:12px;letter-spacing:.12em;text-transform:uppercase}code,kbd,pre{font-family:var(--fn-mono)!important}#content-area :not(pre)>code{border:1px solid var(--fn-border);border-radius:6px;padding:1px 6px}#content-area .card,#content-area .code-block,#content-area [class*=callout],#content-area pre,#topbar-cta-button a,#topbar-cta-button a>span{border-radius:10px}#content-area a.card:hover{border-color:var(--fn-hl-border)}#topbar-cta-button a>span:first-child{background:var(--fn-accent)!important}#topbar-cta-button a span,#topbar-cta-button a svg{color:var(--fn-on-accent)!important;font-weight:600}"}],"page":{"pagePath":"hacking/stripe-fake","slug":"/hacking/stripe-fake","pageMetadata":{"title":"Stripe fake","description":"Run billing code against a local Stripe: the API subset billing uses, test clocks, signed webhooks and a control API.","href":"/hacking/stripe-fake"},"description":{"compiledSource":"\"use strict\";\nconst {jsx: _jsx} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n p: \"p\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsx(_components.p, {\n children: \"Run billing code against a local Stripe: the API subset billing uses, test clocks, signed webhooks and a control API.\"\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx,jsxs:_jsxs}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){const _components={code:`code`,li:`li`,p:`p`,pre:`pre`,span:`span`,strong:`strong`,tbody:`tbody`,td:`td`,th:`th`,thead:`thead`,tr:`tr`,ul:`ul`,..._provideComponents(),...props.components},{CodeBlock,Heading,Table,Warning}=_components;if(!CodeBlock)_missingMdxReference(`CodeBlock`,true);if(!Heading)_missingMdxReference(`Heading`,true);if(!Table)_missingMdxReference(`Table`,true);if(!Warning)_missingMdxReference(`Warning`,true);return _jsxs(_Fragment,{children:[_jsxs(_components.p,{children:[`The Stripe fake answers the part of the Stripe API that Fonoster’s billing uses, so billing code and\nits tests run with no Stripe account and no network. It lives in `,_jsx(_components.code,{children:`mods/stripe-fake`}),`.`]}),`\n`,_jsxs(_components.p,{children:[`Its answers follow Stripe’s own OpenAPI schema at a pinned version (API `,_jsx(_components.code,{children:`2026-09-30.endive`}),`,\n`,_jsx(_components.code,{children:`stripe`}),` npm 23), and its tests check every response against that schema. It covers customers and\npayment methods, products and prices, billing meters and meter events, credit grants,\nsubscriptions and invoices under test clocks, payment intents and refunds, Customer Portal\nsessions, events, and signed\nwebhooks.`]}),`\n`,_jsx(Warning,{children:_jsxs(_components.p,{children:[`The fake is for development and tests only. It keeps everything in memory and accepts any\n`,_jsx(_components.code,{children:`sk_test_`}),` key.`]})}),`\n`,_jsx(Heading,{level:`2`,id:`run-it`,children:`Run it`}),`\n`,_jsxs(_components.p,{children:[`With Docker Compose, the fake is a service on the `,_jsx(_components.code,{children:`fakes`}),` profile:`]}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`1`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`1`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`docker`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` compose`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -f`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` ops/rebuild/compose.yaml`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` --profile`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` fakes`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` up`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` stripe-fake`})]}),`\n`]})})}),`\n`,_jsxs(_components.p,{children:[`It listens on port `,_jsx(_components.code,{children:`12111`}),` (set `,_jsx(_components.code,{children:`RB_STRIPE_FAKE_PORT`}),` to change the host port). From source:`]}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`2`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`2`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`npm`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` run`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` build`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -w`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` mods/stripe-fake`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`node`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` mods/stripe-fake/dist/main.js`})]}),`\n`]})})}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Variable`}),_jsx(_components.th,{children:`Default`}),_jsx(_components.th,{children:`What it does`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`STRIPE_FAKE_PORT`})}),_jsx(_components.td,{\"data-numeric\":`true`,children:_jsx(_components.code,{children:`12111`})}),_jsx(_components.td,{children:`Port to listen on`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`STRIPE_FAKE_WEBHOOKS`})}),_jsx(_components.td,{children:`none`}),_jsx(_components.td,{children:`Webhook endpoints to create at start (see below)`})]})]})]}),`\n`,_jsx(Heading,{level:`2`,id:`watch-it`,children:`Watch it`}),`\n`,_jsxs(_components.p,{children:[`Open `,_jsx(_components.code,{children:`http://localhost:12111`}),` for the viewer: what Stripe would see, updated live as your code or\ntests call the fake.`]}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Customers`}),`: the credit balance (1 credit = $0.01), plan and top-up credits, meter totals and\nrecent webhooks.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Meters`}),`: each meter and its recent events, accepted or rejected.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Credit grants`}),`, `,_jsx(_components.strong,{children:`Invoices`}),` (with line items) and `,_jsx(_components.strong,{children:`Webhooks`}),` (each delivery’s attempts,\npayload and a Replay button).`]}),`\n`]}),`\n`,_jsx(_components.p,{children:`The top bar moves the test clock a month ahead and switches between light and dark. The sidebar\ncan make the next payment fail or reset everything.`}),`\n`,_jsxs(_components.p,{children:[`To work on the viewer itself, run a fake on `,_jsx(_components.code,{children:`12111`}),` and then:`]}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`1`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`1`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`npm`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` run`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` dev:viewer`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` -w`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` mods/stripe-fake`})]}),`\n`]})})}),`\n`,_jsxs(_components.p,{children:[`It serves on `,_jsx(_components.code,{children:`12112`}),` and sends API calls to the fake (`,_jsx(_components.code,{children:`STRIPE_FAKE_URL`}),` to change it).`]}),`\n`,_jsx(Heading,{level:`2`,id:`point-the-stripe-sdk-at-it`,children:`Point the Stripe SDK at it`}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`9`,language:`typescript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`typescript`,children:_jsxs(_components.code,{language:`typescript`,numberOfLines:`9`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`import`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` Stripe`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` from`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` \"stripe\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`;`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`const`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4FC1FF`},children:` stripe`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:` new`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:` Stripe`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"sk_test_anything\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, {`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` host:`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` \"127.0.0.1\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`,`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` port:`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#B5CEA8`},children:` 12111`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`,`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` protocol:`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` \"http\"`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`});`})}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`const`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4FC1FF`},children:` customer`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` await`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` stripe`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`customers`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`create`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`({ `}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`email:`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` \"ada@acme.com\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` });`})]}),`\n`]})})}),`\n`,_jsxs(_components.p,{children:[`Requests need a `,_jsx(_components.code,{children:`sk_test_`}),` or `,_jsx(_components.code,{children:`rk_test_`}),` key. A `,_jsx(_components.code,{children:`Stripe-Version`}),` header, when sent, must be the\npinned version. Unknown parameters and paths are refused with Stripe’s error shape, and\n`,_jsx(_components.code,{children:`Idempotency-Key`}),` replays work as in Stripe.`]}),`\n`,_jsxs(_components.p,{children:[`Stripe’s test payment methods work: `,_jsx(_components.code,{children:`pm_card_visa`}),`, `,_jsx(_components.code,{children:`pm_card_mastercard`}),`,\n`,_jsx(_components.code,{children:`pm_card_chargeDeclined`}),`, `,_jsx(_components.code,{children:`pm_card_chargeDeclinedInsufficientFunds`}),` and\n`,_jsx(_components.code,{children:`pm_card_authenticationRequired`}),`.`]}),`\n`,_jsx(Heading,{level:`2`,id:`move-time`,children:`Move time`}),`\n`,_jsx(_components.p,{children:`Create a test clock and attach customers to it, as in Stripe. Advancing the clock runs the\nbilling cycle: renewals, invoices finalized after the grace period (one hour by default), credit\ngrants applied to metered lines, payment, and retries after 3, 5 and 7 days.`}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`4`,language:`typescript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`typescript`,children:_jsxs(_components.code,{language:`typescript`,numberOfLines:`4`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`const`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4FC1FF`},children:` clock`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` await`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` stripe`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`testHelpers`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`testClocks`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`create`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`({ `}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`frozen_time:`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` start`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` });`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`const`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4FC1FF`},children:` customer`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` await`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` stripe`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`customers`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`create`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`({ `}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`test_clock:`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` clock`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`id`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` });`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`// … subscribe, send meter events …`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`await`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` stripe`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`testHelpers`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`testClocks`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`advance`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`clock`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`id`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, { `}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`frozen_time:`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` periodEnd`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` +`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#B5CEA8`},children:` 7200`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` });`})]}),`\n`]})})}),`\n`,_jsx(Heading,{level:`2`,id:`webhooks`,children:`Webhooks`}),`\n`,_jsxs(_components.p,{children:[`Create endpoints with `,_jsx(_components.code,{children:`stripe.webhookEndpoints.create`}),`, or at start with `,_jsx(_components.code,{children:`STRIPE_FAKE_WEBHOOKS`}),`:\na URL and its signing secret joined by `,_jsx(_components.code,{children:`|`}),`, with commas between endpoints.`]}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`1`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`1`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`STRIPE_FAKE_WEBHOOKS`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:`=`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"http://localhost:3000/stripe|whsec_test\"`}),_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:` node`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` mods/stripe-fake/dist/main.js`})]}),`\n`]})})}),`\n`,_jsxs(_components.p,{children:[`Deliveries are\nsigned like Stripe’s, so `,_jsx(_components.code,{children:`stripe.webhooks.constructEvent`}),` verifies them. A delivery that doesn’t\nget a 2xx is retried with backoff. Meter event errors arrive as thin `,_jsx(_components.code,{children:`v2.core.event`}),` events; read\nthe full event at `,_jsx(_components.code,{children:`GET /v2/core/events/{id}`}),`.`]}),`\n`,_jsx(Heading,{level:`2`,id:`control-api`,children:`Control API`}),`\n`,_jsxs(_components.p,{children:[`Tests set the fake’s state through `,_jsx(_components.code,{children:`/_fake`}),`, which is not part of Stripe’s API. Bodies are JSON.`]}),`\n`,_jsxs(Table,{children:[_jsx(_components.thead,{children:_jsxs(_components.tr,{children:[_jsx(_components.th,{children:`Request`}),_jsx(_components.th,{children:`What it does`})]})}),_jsxs(_components.tbody,{children:[_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`GET /_fake/state`})}),_jsx(_components.td,{children:`Everything the fake holds`})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`POST /_fake/reset`})}),_jsx(_components.td,{children:`Empties the fake and starts ids over`})]}),_jsxs(_components.tr,{children:[_jsxs(_components.td,{children:[_jsx(_components.code,{children:`GET /_fake/snapshot`}),`, `,_jsx(_components.code,{children:`POST /_fake/restore`})]}),_jsx(_components.td,{children:`Saves and restores the whole state`})]}),_jsxs(_components.tr,{children:[_jsxs(_components.td,{children:[_jsx(_components.code,{children:`POST /_fake/clock/advance`}),` `,_jsx(_components.code,{children:`{seconds}`})]}),_jsx(_components.td,{children:`Moves the fake’s time and every test clock forward`})]}),_jsxs(_components.tr,{children:[_jsxs(_components.td,{children:[_jsx(_components.code,{children:`POST /_fake/customers/{id}/next_payment`}),` `,_jsx(_components.code,{children:`{outcome}`})]}),_jsxs(_components.td,{children:[`The customer’s next payment `,_jsx(_components.code,{children:`succeed`}),`s, gets a `,_jsx(_components.code,{children:`decline`}),`, `,_jsx(_components.code,{children:`requires_action`}),` or a `,_jsx(_components.code,{children:`timeout`})]})]}),_jsxs(_components.tr,{children:[_jsxs(_components.td,{children:[_jsx(_components.code,{children:`POST /_fake/webhooks/fail_next`}),` `,_jsx(_components.code,{children:`{n}`})]}),_jsxs(_components.td,{children:[`The next `,_jsx(_components.code,{children:`n`}),` deliveries fail`]})]}),_jsxs(_components.tr,{children:[_jsx(_components.td,{children:_jsx(_components.code,{children:`POST /_fake/webhooks/replay/{event}`})}),_jsx(_components.td,{children:`Delivers an event again`})]}),_jsxs(_components.tr,{children:[_jsxs(_components.td,{children:[_jsx(_components.code,{children:`POST /_fake/meters/inject_error_report`}),` `,_jsx(_components.code,{children:`{meter, code}`})]}),_jsx(_components.td,{children:`Sends a meter error report`})]}),_jsxs(_components.tr,{children:[_jsxs(_components.td,{children:[_jsx(_components.code,{children:`POST /_fake/latency`}),` `,_jsx(_components.code,{children:`{ms}`})]}),_jsx(_components.td,{children:`Delays every answer`})]}),_jsxs(_components.tr,{children:[_jsxs(_components.td,{children:[_jsx(_components.code,{children:`POST /_fake/rate_limit`}),` `,_jsx(_components.code,{children:`{on}`})]}),_jsxs(_components.td,{children:[`Answers `,_jsx(_components.code,{children:`429`}),` while on`]})]}),_jsxs(_components.tr,{children:[_jsxs(_components.td,{children:[_jsx(_components.code,{children:`POST /_fake/config`}),` `,_jsx(_components.code,{children:`{grace_seconds, tax_rates}`})]}),_jsx(_components.td,{children:`Sets the grace period and tax rates`})]})]})]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.code,{children:`/_fake/ws`}),` is a WebSocket that sends a message on every change, for watching the fake live.`]}),`\n`,_jsx(Heading,{level:`2`,id:`run-its-tests`,children:`Run its tests`}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`1`,language:`shellscript`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`shellscript`,children:_jsxs(_components.code,{language:`shellscript`,numberOfLines:`1`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#DCDCAA`},children:`npx`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` mocha`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` mods/stripe-fake/test/stripeFake.test.ts`})]}),`\n`]})})}),`\n`,_jsxs(_components.p,{children:[`The tests drive the fake through the official Stripe SDK. To refresh the schema after moving to a\nnew Stripe API version, change `,_jsx(_components.code,{children:`OPENAPI_RELEASE`}),` in `,_jsx(_components.code,{children:`scripts/extract-openapi.ts`}),` and run\n`,_jsx(_components.code,{children:`npm run extract:openapi -w mods/stripe-fake`}),`.`]})]})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent};function _missingMdxReference(id,component){throw new Error(`Expected `+(component?`component`:`object`)+\" `\"+id+\"` to be defined: you likely forgot to import, pass, or provide it.\")}","frontmatter":{},"scope":{"codeStyling":"system","config":{},"pageMetadata":{"title":"Stripe fake","description":"Run billing code against a local Stripe: the API subset billing uses, test clocks, signed webhooks and a control API.","href":"/hacking/stripe-fake"}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[{"title":"Run it","slug":"run-it","depth":2,"children":[]},{"title":"Watch it","slug":"watch-it","depth":2,"children":[]},{"title":"Point the Stripe SDK at it","slug":"point-the-stripe-sdk-at-it","depth":2,"children":[]},{"title":"Move time","slug":"move-time","depth":2,"children":[]},{"title":"Webhooks","slug":"webhooks","depth":2,"children":[]},{"title":"Control API","slug":"control-api","depth":2,"children":[]},{"title":"Run its tests","slug":"run-its-tests","depth":2,"children":[]}],"codeExamples":{}},"jsLinks":[],"subdomain":"fonoster","siteName":"Fonoster","title":"Stripe fake - Fonoster","navMaps":{"firstHrefInVersion":{"v0.30":{"href":"/hacking/stripe-fake","matchLevel":0},"Legacy (≤0.27)":{"href":"/legacy/introduction","matchLevel":4}},"firstHrefInLanguage":{}},"lastModified":"2026-10-24T12:22:56.310Z","originalFileLocation":"hacking/stripe-fake.mdx"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=null;if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"dark\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"Run billing code against a local Stripe: the API subset billing uses, test clocks, signed webhooks and a control API.\">\u003cmeta data-mint-head name=\"application-name\" content=\"Fonoster\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/fonoster/znu2Q2jYdOZXwpaB/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/mintlify-assets/_mintlify/favicons/fonoster/znu2Q2jYdOZXwpaB/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/fonoster/znu2Q2jYdOZXwpaB/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/fonoster/znu2Q2jYdOZXwpaB/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/fonoster/znu2Q2jYdOZXwpaB/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/fonoster/znu2Q2jYdOZXwpaB/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/fonoster/znu2Q2jYdOZXwpaB/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/fonoster/znu2Q2jYdOZXwpaB/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/fonoster/znu2Q2jYdOZXwpaB/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Stripe fake - Fonoster\">\u003cmeta data-mint-head property=\"og:description\" content=\"Run billing code against a local Stripe: the API subset billing uses, test clocks, signed webhooks and a control API.\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"Fonoster\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://docs.fonoster.com/hacking/stripe-fake\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://fonoster.mintlify.app/_mintlify/api/og/v2?division=Start\u0026amp;title=Stripe+fake\u0026amp;description=Run+billing+code+against+a+local+Stripe%3A+the+API+subset+billing+uses%2C+test+clocks%2C+signed+webhooks+and+a+control+API.\u0026amp;theme=609b6b2300df115f2cdedc91\u0026amp;signature=90b8d1c00787cc136591d588cba82ab733233f4e610d608ff9269b90436f2918\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Stripe fake - Fonoster\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"Run billing code against a local Stripe: the API subset billing uses, test clocks, signed webhooks and a control API.\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://fonoster.mintlify.app/_mintlify/api/og/v2?division=Start\u0026amp;title=Stripe+fake\u0026amp;description=Run+billing+code+against+a+local+Stripe%3A+the+API+subset+billing+uses%2C+test+clocks%2C+signed+webhooks+and+a+control+API.\u0026amp;theme=609b6b2300df115f2cdedc91\u0026amp;signature=90b8d1c00787cc136591d588cba82ab733233f4e610d608ff9269b90436f2918\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/mintlify-assets/_mintlify/favicons/fonoster/znu2Q2jYdOZXwpaB/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"Fonoster\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#008751\">\u003clink data-mint-head rel=\"canonical\" href=\"https://docs.fonoster.com/hacking/stripe-fake\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/hacking/stripe-fake.md\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://docs.fonoster.com/#organization\",\"name\":\"Fonoster\",\"url\":\"https://docs.fonoster.com\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://mintcdn.com/fonoster/GsxtawNPYOQvohG4/logo/light.svg?fit=max\u0026auto=format\u0026n=GsxtawNPYOQvohG4\u0026q=85\u0026s=4b95dca8d9d982d3851611f31fb2f3f6\"}},{\"@type\":\"WebSite\",\"@id\":\"https://docs.fonoster.com#website\",\"name\":\"Fonoster\",\"url\":\"https://docs.fonoster.com\",\"publisher\":{\"@id\":\"https://docs.fonoster.com/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://docs.fonoster.com/hacking/stripe-fake#webpage\",\"url\":\"https://docs.fonoster.com/hacking/stripe-fake\",\"name\":\"Stripe fake\",\"description\":\"Run billing code against a local Stripe: the API subset billing uses, test clocks, signed webhooks and a control API.\",\"dateModified\":\"2026-10-24T12:22:56.310Z\",\"isPartOf\":{\"@id\":\"https://docs.fonoster.com#website\"},\"breadcrumb\":{\"@id\":\"https://docs.fonoster.com/hacking/stripe-fake#breadcrumb\"}},{\"@type\":\"BreadcrumbList\",\"@id\":\"https://docs.fonoster.com/hacking/stripe-fake#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Hacking Fonoster\",\"item\":\"https://docs.fonoster.com/hacking/overview\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Start\",\"item\":\"https://docs.fonoster.com/hacking/overview\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Stripe fake\",\"item\":\"https://docs.fonoster.com/hacking/stripe-fake\"}]},{\"@type\":[\"Article\",\"TechArticle\"],\"@id\":\"https://docs.fonoster.com/hacking/stripe-fake#article\",\"headline\":\"Stripe fake\",\"name\":\"Stripe fake\",\"description\":\"Run billing code against a local Stripe: the API subset billing uses, test clocks, signed webhooks and a control API.\",\"url\":\"https://docs.fonoster.com/hacking/stripe-fake\",\"mainEntityOfPage\":{\"@id\":\"https://docs.fonoster.com/hacking/stripe-fake#webpage\"},\"dateModified\":\"2026-10-24T12:22:56.310Z\",\"publisher\":{\"@id\":\"https://docs.fonoster.com/#organization\"},\"isPartOf\":{\"@id\":\"https://docs.fonoster.com#website\"}}]}\u003c/script>\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"Fonoster\",\"creator\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","siteName":"Fonoster","nav":{"global":{"anchors":[{"anchor":"GitHub","icon":"github","href":"https://github.com/fonoster/fonoster"},{"anchor":"Community","icon":"discord","href":"https://discord.gg/4QWgSz4hTC"}]},"versions":[{"version":"v0.30","default":true,"tabs":[{"tab":"Docs","groups":[{"group":"Get started","pages":[{"title":"Introduction","href":"/introduction"},{"title":"Your first AI call","sidebarTitle":"Quickstart","href":"/quickstart"},{"title":"How Fonoster works","href":"/concepts/how-fonoster-works"},{"title":"Migrate from 0.27","href":"/migrate-from-0-27"}]},{"group":"Autopilot","pages":[{"title":"Build an Autopilot agent","sidebarTitle":"Build an agent","href":"/autopilot/build-an-agent"},{"title":"Give your agent tools","sidebarTitle":"Tools","href":"/autopilot/tools"},{"title":"Add a knowledge base","sidebarTitle":"Knowledge base","href":"/autopilot/knowledge-base"},{"title":"Test your agent","sidebarTitle":"Test","href":"/autopilot/test"},{"title":"Write prompts for voice","sidebarTitle":"Prompting","href":"/autopilot/prompting"},{"title":"Turn-taking and barge-in","href":"/autopilot/turn-taking"}]},{"group":"Streams","pages":[{"title":"Build a stream with the Voice SDK","sidebarTitle":"Voice SDK (Node.js)","href":"/streams/voice-sdk"},{"title":"Use the Python Voice SDK","sidebarTitle":"Python Voice SDK","href":"/streams/python"},{"title":"Bring your Twilio or Pipecat app","href":"/streams/bring-your-app"},{"title":"Verify request signatures","sidebarTitle":"Verify signatures","href":"/streams/verify-signatures"}]},{"group":"Telephony","pages":[{"title":"Numbers and routing","href":"/telephony/numbers"},{"title":"Link a Twilio number","href":"/telephony/link-twilio"},{"title":"Connect a SIP trunk","href":"/telephony/trunks"},{"title":"SIP agents (softphones)","sidebarTitle":"SIP agents","href":"/telephony/sip-agents"},{"title":"Make outbound calls","href":"/telephony/outbound-calls"},{"title":"Transfer calls","href":"/telephony/transfers"},{"title":"DTMF (keypad tones)","sidebarTitle":"DTMF","href":"/telephony/dtmf"},{"title":"Detect answering machines","href":"/telephony/amd"},{"title":"Record calls and ask for consent","sidebarTitle":"Record calls","href":"/telephony/recording"},{"title":"Cancel background noise","sidebarTitle":"Noise cancelling","href":"/telephony/noise-cancelling"}]},{"group":"Observe","pages":[{"title":"Receive events with webhooks","sidebarTitle":"Webhooks","href":"/observe/webhooks"},{"title":"Read and verify call records","sidebarTitle":"Call records","href":"/observe/call-records"},{"title":"Retention and erasure","href":"/observe/retention"},{"title":"See what your workspace used","sidebarTitle":"Usage","href":"/observe/usage"}]},{"group":"Workspace","pages":[{"title":"Workspaces, members and API keys","sidebarTitle":"Access","href":"/workspace/access"},{"title":"Use your own vendor keys","sidebarTitle":"Vendor keys","href":"/workspace/provider-keys"},{"title":"Secrets","href":"/workspace/secrets"},{"title":"Use Fonoster from an AI client (MCP)","sidebarTitle":"MCP server","href":"/workspace/mcp"}]},{"group":"Reference","pages":[{"title":"Autopilot configuration","href":"/reference/autopilot-config"},{"title":"Webhook events","href":"/reference/webhook-events"},{"title":"Providers and models","href":"/reference/providers"}]},{"group":"Self-hosting","icon":"server","pages":[{"group":"Start","pages":[{"title":"Overview","href":"/self-hosting/overview"},{"title":"Install with Docker Compose","href":"/self-hosting/install-compose"}]},{"group":"Operate","pages":[{"title":"Manage the provider catalog","sidebarTitle":"Provider catalog","href":"/self-hosting/provider-catalog"},{"title":"Rotate call-record keys","sidebarTitle":"Call-record keys","href":"/self-hosting/call-record-keys"},{"title":"Tune turn-taking and audio","sidebarTitle":"Turn-taking and audio","href":"/self-hosting/turn-taking-and-audio"}]},{"group":"Integrations","pages":[{"title":"Send call metrics to ClickHouse","href":"/self-hosting/send-call-metrics-to-clickhouse"},{"title":"Send telemetry","href":"/self-hosting/send-telemetry"}]}]},{"group":"Private regions","icon":"globe","pages":[{"group":"Private regions","pages":[{"title":"What a private region is","href":"/private-regions/overview"}]},{"group":"Control plane","pages":[{"title":"Host private regions","href":"/private-regions/host-private-regions"}]}]},{"group":"Hacking Fonoster","icon":"code","pages":[{"group":"Start","pages":[{"title":"Overview","href":"/hacking/overview"},{"title":"Run Fonoster from source","description":"Open the dev container, run every component from source, and call an agent from your browser.","href":"/hacking/run-from-source"},{"title":"Stripe fake","href":"/hacking/stripe-fake"}]}]}]},{"tab":"SDK","pages":[{"title":"SDK","href":"/reference/sdk"}]},{"tab":"CLI","pages":[{"title":"CLI","href":"/reference/cli"}]},{"tab":"Changelog","pages":[{"title":"Changelog","href":"/changelog"}]}]},{"version":"Legacy (≤0.27)","pages":[{"title":"Legacy (≤0.27)","href":"/legacy/introduction"}]}]},"title":"Stripe fake - Fonoster","buildId":"dpl_HcHmUS6i3UwswnTHJUEdcTtwfdTQ","deploymentId":"dpl_HcHmUS6i3UwswnTHJUEdcTtwfdTQ","dynamics":[],"chunks":[]}