Skip to content

Latest commit

 

History

History
418 lines (359 loc) · 49.8 KB

File metadata and controls

418 lines (359 loc) · 49.8 KB

Compatibility

Billtap is a fixture-backed billing lab for local development and CI. Its goal is measurable Stripe API compatibility for a growing, documented subset, plus local checkout, portal, webhook, and scenario tooling. It is not a payment processor or a Stripe Dashboard replacement.

The compatibility promise is scoped but parity-driven:

  • Stripe-compatible request validation and response shapes for documented endpoints, backed by tests and scorecard cases.
  • Hosted sandbox checkout and portal flows for exercising app integration code.
  • Webhook delivery evidence for retries, duplicate delivery, delay, out-of-order delivery, and replay.
  • Scenario and fixture APIs that make the supported subset repeatable.

Anything outside this document should be treated as unsupported until it has a fixture, a test, and an explicit compatibility note.

Known Stripe OpenAPI routes that are not implemented by Billtap first run OpenAPI-derived parameter validation. Malformed requests return Stripe-shaped parameter_unknown, parameter_missing, or parameter_invalid errors; valid but unimplemented requests return unsupported_endpoint instead of silently approximating provider behavior. When diagnostics are enabled, the request trace captures that error code and the original path so agents can distinguish bad test setup, unsupported coverage gaps, missing local data, and webhook failures.

Compatibility Levels

Level Meaning
Supported Implemented in the local runtime and covered by automated tests or release-blocking compatibility scorecard cases. Examples and fixtures are supporting evidence, not the sole basis for a supported claim.
Billtap-specific Public Billtap API, not intended to match Stripe.
Partial Useful for smoke tests, but not a full provider behavior model.
Unsupported Not implemented, not claimed, or intentionally out of scope.

Compatibility Scorecard

The offline compatibility scorecard can be generated without external services:

go run ./cmd/billtap compatibility scorecard --output-dir dist/compatibility

It writes:

  • compatibility-scorecard.json
  • compatibility-scorecard.md
  • replay-bundles/*.json for any mismatch or error case

Scorecard statuses are:

  • imported: case ran against Billtap and matched the normalized expectation.
  • skipped: case is in the corpus but intentionally not run by the offline lane.
  • unsupported: case documents unsupported behavior from this compatibility contract.
  • mismatch: case ran but normalized actual behavior differed from expectation.
  • error: the scorecard runner or Billtap returned an unexpected internal error.

Current public-readiness corpus:

  • Scorecard version: l3-public-readiness-v7
  • Release-blocking cases: 49
  • Covered categories: request validation, protocol parameter acceptance, OpenAPI-backed fallback validation, idempotency mismatch, deterministic checkout payment-error aliases, and direct PaymentIntent/SetupIntent state transitions, invoice retry/payment mutation, and local clock-driven renewal and period-end cancellation scenarios
  • Required release result: mismatch=0, error=0, and passed=true

The scorecard is the release contract for Billtap's documented compatibility subset. Broader Stripe API parity is expanded incrementally through the roadmap instead of being implied before a surface has tests and fixtures.

Broader Stripe API compatibility work is tracked separately in STRIPE_API_COMPATIBILITY_ROADMAP.md. New endpoint families should move through inventory, schema validation, fixture response, stateful local behavior, scenario coverage, webhook modeling, and SDK smoke levels before they become public claims.

The generated Stripe API inventory also reports summary.schema_validated_operations, which counts operations from the input OpenAPI file that expose parameter or request-body schemas and also match Billtap's bundled OpenAPI-derived validation catalog. This is a diagnostic and planning metric only. It does not increase summary.implemented_operations; an endpoint still needs an explicit runtime claim, tests, fixtures, and documentation before it counts as implemented.

Supported Stripe-Like API Subset

Base path: /v1. A parallel test run can scope the same Stripe-like API under /runs/<runId>/v1; unscoped requests use the backward-compatible default run. When PUBLIC_BASE_PATH or BILLTAP_PUBLIC_BASE_PATH is set, or a proxy sends X-Forwarded-Prefix, the same API is available below that browser-facing prefix, such as /billtap/v1 or /billtap/runs/<runId>/v1. Internal service traffic can keep using the unprefixed service URL.

Resource Endpoints Level Scope
Customers POST /v1/customers, GET /v1/customers, GET /v1/customers/search, GET /v1/customers/{id}, POST /v1/customers/{id} Supported Create, list, retrieve, update, and search email, name, and metadata. List supports email and limit filters. Search supports a measured Stripe Search Query Language subset for id, email, name, and metadata equality clauses joined by AND.
Products POST /v1/products, GET /v1/products, GET /v1/products/{id}, POST /v1/products/{id}, DELETE /v1/products/{id} Supported Create, list, retrieve, update, and delete local service products with metadata. Deletion removes the product; existing prices keep referencing the deleted product id.
Product search GET /v1/products/search Partial Supports metadata equality filters such as metadata['tenantId']:'saas' and active:'true'. This is not Stripe Search Query Language parity.
Prices POST /v1/prices, GET /v1/prices, GET /v1/prices/{id}, POST /v1/prices/{id}, GET /v1/prices/search Supported Create, list, retrieve, update, and search prices. Update accepts transfer_lookup_key to move a key another price holds. Supports product, currency, unit_amount, lookup_key, recurring interval fields, active, and metadata. List supports product, active, type=recurring, and type=one_time. Search supports a measured Stripe Search Query Language subset for active, type, lookup_key, and metadata equality clauses joined by AND, including one-time price lookup by lookup_key.
Coupons POST /v1/coupons, GET /v1/coupons, GET /v1/coupons/{id}, POST /v1/coupons/{id}, DELETE /v1/coupons/{id} Partial Create/list/retrieve local coupon evidence for SDK and fixture smoke paths. Amount-off, percent-off, duration, metadata, and deletion markers are modeled. A bounded single-discount subset applies coupons to customer defaults, checkout sessions, subscriptions, invoice preview math, and renewal invoices; redemption limits and expiration analytics are not modeled.
Promotion codes POST /v1/promotion_codes, GET /v1/promotion_codes, GET /v1/promotion_codes/{id}, POST /v1/promotion_codes/{id} Partial Create/list/retrieve local promotion-code evidence linked to a local coupon. List supports code, coupon, customer, and active filters. Promotion codes can be applied through discounts[0][promotion_code]; minimum amounts, expiration rules, and promotion analytics are not modeled.
Tax rates GET/POST /v1/tax_rates, GET/POST /v1/tax_rates/{id} Partial Create/list/retrieve/update local tax-rate evidence. Checkout subscription_data[default_tax_rates][] and subscription default_tax_rates resolve txr_* IDs and snapshot inclusive/exclusive rates onto sessions, subscriptions, invoices, renewal/proration math, and previews, applied after discounts; automatic_tax is mutually exclusive. Fixture packs can seed tax rates with explicit IDs. Jurisdiction and address-based calculation are not modeled; automatic_tax remains a customer-metadata tax_percent simulation.
Customer tax IDs GET/POST /v1/customers/{id}/tax_ids, GET/DELETE /v1/customers/{id}/tax_ids/{id} Partial Local customer tax-ID evidence store backing checkout tax_id_collection[enabled] flows. Provider verification is not modeled.
Checkout sessions POST /v1/checkout/sessions, GET /v1/checkout/sessions, GET /v1/checkout/sessions/{id}, POST /v1/checkout/sessions/{id}, POST /v1/checkout/sessions/{id}/expire, GET /v1/checkout/sessions/{id}/line_items Supported Creates subscription- and payment-mode sandbox checkout sessions from request line items and hosted Billtap URLs; setup mode is rejected. The Stripe-style session response leaves line_items unexpanded; GET .../line_items returns them as expanded item objects with pre-discount amounts (per-line discount/tax splits are not modeled). Accepts Stripe SDK form params allow_promotion_codes, discounts[0][coupon], discounts[0][promotion_code], subscription_data[trial_period_days], and subscription_data[default_tax_rates] (applied to session totals; exclusive/inclusive snapshots; mutually exclusive with automatic_tax); trial checkout creates local trialing subscription evidence. Payment-mode sessions accept inline price_data line items (creating local product/price evidence), payment_intent_data[...], client_reference_id, session-level metadata[...], automatic_tax[enabled], and tax_id_collection[enabled] (tax snapshot from customer metadata tax_percent), and complete into a one-time PaymentIntent without subscription or invoice evidence. POST .../sessions/{id} updates an open session (metadata merge plus line_items[N][quantity] overrides with immutable prices); expire moves an open session to expired and emits checkout.session.expired; non-open sessions return invalid_request_error. Hosted URLs use the request host by default, or BILLTAP_PUBLIC_BASE_URL when configured for container-to-host browser flows. PUBLIC_BASE_PATH, BILLTAP_PUBLIC_BASE_PATH, and X-Forwarded-Prefix are reflected in hosted browser URLs.
Checkout completion POST /v1/checkout/sessions/{id}/complete, POST /api/checkout/sessions/{id}/complete, POST/DELETE /api/checkout/sessions/{id}/promotion_code Billtap-specific Completes a sandbox checkout and creates subscription, invoice, payment intent, timeline, and checkout webhook evidence. Supports success plus deterministic failure aliases such as card_declined, insufficient_funds, expired_card, incorrect_cvc, processing_error, authentication_required, payment_pending, canceled, and documented Stripe test PaymentMethod IDs such as pm_card_visa_chargeDeclined. Hosted checkout can apply or remove a promotion code on an open allow_promotion_codes session via POST/DELETE /api/checkout/sessions/{id}/promotion_code (form promotion_code=<code>), refreshing session totals before completion.
Billing portal sessions POST /v1/billing_portal/sessions Partial Returns a Stripe-like billing_portal.session object and Billtap portal URL for a known customer, including flow, locale, on_behalf_of, return_url, and livemode fields. Accepts customer, return_url, optional configuration, locale, on_behalf_of, and flow_data, with enum and required nested-field validation for the modeled portal flows. The hosted portal can simulate saved payment-method updates and subscription cancellation, then redirect to return_url; full Stripe-hosted portal configuration rendering is not modeled.
Billing portal configurations GET/POST /v1/billing_portal/configurations, GET/POST /v1/billing_portal/configurations/{id} Partial Local portal-configuration evidence with Stripe-shaped business_profile, default_return_url, login_page, features[...], and metadata. Create applies Stripe-shaped feature defaults; update merges business profile, features, and metadata and bumps updated. List filters by active and is_default; the first configuration in a run becomes the default. The hosted portal does not render configuration features.
Subscriptions POST /v1/subscriptions, GET /v1/subscriptions, GET /v1/subscriptions/search, GET /v1/subscriptions/{id}, POST /v1/subscriptions/{id}, DELETE /v1/subscriptions/{id}, POST /v1/subscriptions/{id}/resume, POST /v1/subscriptions/{id}/migrate (billing-mode evidence), nested GET/POST/DELETE /v1/customers/{id}/subscriptions..., GET/DELETE /v1/subscriptions/{id}/discount Partial Create/list/retrieve subscriptions through the local checkout-completion state path. Update supports item replacement, metadata merge, single coupon or promotion-code discount application (discount objects serialize the Stripe SDK 31 source.{coupon,promotion_code} shape alongside the legacy top-level coupon), test_clock, cancel_at_period_end, pause_collection, and accepted billing/proration evidence including proration_date, proration_behavior, payment_behavior, billing_cycle_anchor, and trial_end; resume clears local pause evidence and emits customer.subscription.updated. Top-level and nested customer lists support status and metadata equality filters such as metadata[billtap_fixture_ref]. Search supports id, customer, status, and metadata equality clauses joined by AND. Delete performs immediate sandbox cancellation. Test-clock and scenario clock advances can activate due trials, renew active periods with bounded discount math, fail configured renewals from subscription or customer metadata, and cancel period-end subscriptions in the local billing graph.
Subscription schedules POST /v1/subscription_schedules, GET /v1/subscription_schedules, GET /v1/subscription_schedules/{id}, POST /v1/subscription_schedules/{id}, POST /v1/subscription_schedules/{id}/cancel, POST /v1/subscription_schedules/{id}/release Partial Local schedule evidence for one due phase on an existing subscription. Test-clock advance applies a due phase by replacing subscription items and emits customer.subscription.updated; complex multi-phase billing, proration, invoices, and schedule lifecycle parity are not modeled.
Subscription items POST /v1/subscription_items, GET /v1/subscription_items, GET /v1/subscription_items/{id}, POST /v1/subscription_items/{id}, DELETE /v1/subscription_items/{id} Partial List, retrieve, add, update, and remove local subscription items. List supports the subscription filter; update replaces price/quantity and echoes metadata and evidence-only tax_rates. Create, update, and delete accept proration_behavior (none/create_prorations/always_invoice) and proration_date through the same proration path as subscription update, applying the subscription's default_tax_rates after discounts: always_invoice issues a paid subscription_update invoice and repoints latest_invoice, and create_prorations defers the delta to the next renewal invoice. Deleting the last item is rejected. Item IDs are stored at creation so deletes no longer shift later IDs. Item-level tax_rates are evidence-only, and metered usage is not modeled.
Invoices POST /v1/invoices, GET /v1/invoices, GET /v1/invoices/search, GET /v1/invoices/{id}, POST /v1/invoices/{id}, DELETE /v1/invoices/{id}, POST /v1/invoices/{id}/finalize, POST /v1/invoices/{id}/pay, POST /v1/invoices/{id}/send, POST /v1/invoices/{id}/void, POST /v1/invoices/{id}/mark_uncollectible, POST /v1/invoices/{id}/attach_payment, POST /v1/invoices/{id}/add_lines, POST /v1/invoices/{id}/update_lines, POST /v1/invoices/{id}/remove_lines, POST /v1/invoices/{id}/lines/{line_item_id}, GET /v1/invoices/{id}/lines, GET /v1/invoices/{id}/payments, POST /v1/invoices/create_preview, GET/POST /v1/invoices/upcoming Partial List/retrieve/search invoices created by checkout and create a bounded manual one-time invoice flow. Search supports id, customer, subscription, and status clauses joined by AND. pay retries open checkout invoices with deterministic sandbox payment_method or source aliases, mutating invoice, subscription, payment-intent, timeline, and webhook evidence. Manual invoices accept customer, currency, collection_method, default_payment_method, description, auto_advance=false, pending_invoice_items_behavior, payment_settings[payment_method_types], and metadata[...]; pending_invoice_items_behavior=include attaches the customer's same-currency pending invoice items once at create. Draft invoices can be updated in place (POST /v1/invoices/{id}: description, days_until_due, default_payment_method, metadata merge) and deleted with their attached lines and timeline evidence (DELETE); add_lines/update_lines/remove_lines (and the single-line POST .../lines/{line_item_id}) mutate draft lines through the invoice-item path and recompute subtotal/total/amount_due; attach_payment records a customer-matched PaymentIntent or payment record on a draft as evidence while collection still runs through finalize/pay. finalize creates local PaymentIntent evidence, and pay applies per-intent or customer-level default outcomes including success, card decline, and requires_action. send records local email evidence and emits invoice.sent; void moves an open invoice to void and emits invoice.voided; mark_uncollectible moves an open invoice to uncollectible and emits invoice.marked_uncollectible; non-open statuses return invalid_request_error. Invoice responses include confirmation_secret, local hosted/PDF paths, payments.data.payment.payment_intent, metadata, status transitions, automatic tax state, tax totals, payment settings, array-shaped discounts, lines.data serialized like the lines endpoint, and post_payment_credit_notes_amount on paid invoices. Preview endpoints calculate a Stripe-shaped subscription-update proration line from current period bounds, old/new price totals, subscription_details[items], subscription_details[proration_date], subscription_details[proration_behavior], and subscription_details[billing_cycle_anchor]; previews without item overrides return the subscription's next billing cycle (items, discounts, and tax with billing_reason: upcoming) matching the next renewal invoice. A bounded single-discount subset updates subtotal, total, and total_discount_amounts; invoice rendering, automatic collection, and full dunning lifecycle are outside the modeled subset.
Invoice items POST /v1/invoiceitems, GET /v1/invoiceitems, GET /v1/invoiceitems/{id} Partial Creates, lists, and retrieves local invoice item evidence for manual one-time invoices. customer, invoice, amount, currency, description, and metadata[...] are accepted, or pricing[price] plus quantity instead of amount (line amount is the price's unit_amount times quantity, currency from the price). Omitting invoice stores the item as a pending customer item that POST /v1/invoices with pending_invoice_items_behavior=include attaches; subscription is accepted and echoed. Tax, discount, and full line mutation parity are not modeled.
Payment intents POST /v1/payment_intents, GET /v1/payment_intents, GET /v1/payment_intents/search, GET /v1/payment_intents/{id}, POST /v1/payment_intents/{id} (metadata/description update), POST /v1/payment_intents/{id}/confirm, POST /v1/payment_intents/{id}/capture, POST /v1/payment_intents/{id}/cancel Partial Create/list/retrieve/search and mutate local payment intents. Search supports id, customer, status, and metadata equality clauses joined by AND. confirm supports deterministic sandbox PaymentMethod aliases such as pm_card_visa, pm_card_visa_chargeDeclined, pm_card_threeDSecure2Required, requires_action, and local bank-transfer processing. Manual capture moves through requires_capture before capture succeeds. Direct one-time intents emit payment_intent.created and a terminal event when confirmed. Deferred per-intent outcomes and customer defaults are supported; requires_action can return either use_stripe_sdk or local redirect_to_url next-action payloads.
Setup intents POST /v1/setup_intents, GET /v1/setup_intents, GET /v1/setup_intents/{id}, POST /v1/setup_intents/{id}/confirm, POST /v1/setup_intents/{id}/cancel Partial Create/list/retrieve and mutate local setup intents with deterministic success, decline, and authentication-required aliases. Mandates, bank-account verification, and full SCA behavior are not modeled.
Payment methods GET/POST /v1/payment_methods, GET/POST /v1/payment_methods/{id}, POST /v1/payment_methods/{id}/attach, POST /v1/payment_methods/{id}/detach, GET /v1/customers/{id}/payment_methods?type=card, GET /v1/customers/{id}/payment_methods/{id} Partial Returns deterministic sandbox card PaymentMethod projections for known customers, including SDK-friendly billing_details, card.checks, country, fingerprint, funding, networks, three_d_secure_usage, metadata, and redaction fields. Query validation covers type, allow_redisplay, limit, and unknown parameters; non-card valid types return an empty local list. Customer fixtures can opt into empty or explicit payment-method lists for no-card and multi-card local scenarios. Create/attach/detach mutate local customer payment-method metadata and reject real card data; general update is accepted as a compatibility no-op for projected local cards.
Customer cash balance GET /v1/customers/{id}/cash_balance, POST /v1/customers/{id}/cash_balance, GET /v1/customers/{id}/cash_balance_transactions, GET /v1/customers/{id}/cash_balance_transactions/{id}, POST /v1/test_helpers/customers/{id}/fund_cash_balance Partial Local cash-balance evidence for bank-transfer smoke tests. The test-helper funding endpoint records a cash-balance transaction and settles processing bank-transfer PaymentIntents for that customer. Banking rails, reconciliation, currency conversion, and customer-balance invoice application are not modeled.
Connect platform evidence GET /v1/account, POST /v1/accounts, GET /v1/accounts, GET /v1/accounts/{id}, POST /v1/accounts/{id}, DELETE /v1/accounts/{id}, POST /v1/account_links, POST /v1/account_sessions, account capabilities, people/persons, external accounts, transfers/reversals, payouts, application fees/refunds Partial Persist local connected-account profiles, capability status, person evidence, bank-account evidence, transfers, transfer reversals, payouts, and application-fee refunds. Account links, account sessions, and login links return local URLs/client secrets. Account deletion returns a local deletion marker. Request traces preserve Stripe-Account routing evidence, and local Connect evidence can emit transfer.*, payout.*, and application_fee.refunded webhooks. KYC, identity verification, bank verification, real onboarding, balance movement, account closure, and settlement behavior are not modeled.
Refunds POST /v1/refunds, GET /v1/refunds, GET/POST /v1/refunds/{id}, POST /v1/refunds/{id}/cancel Partial Create/list/retrieve/update local refund evidence against an invoice, payment intent, or charge-like ID. Omitting amount refunds the full remaining refundable balance on the invoice or payment intent; requested amounts cannot exceed that balance. Creation emits charge.refunded and charge.refund.updated; status updates and test-clock settlement emit charge.refund.updated. Refund fixtures can seed pending and settle on settle_at/available_on. Balance transactions and processor accounting are outside the modeled subset.
Credit notes POST /v1/credit_notes, GET /v1/credit_notes, GET /v1/credit_notes/{id}, GET /v1/credit_notes/{id}/lines, POST /v1/credit_notes/{id}/void Partial Create/list/retrieve/void local credit note evidence for an invoice and emit credit_note.created / credit_note.voided. memo, out_of_band_amount, and refund_amount are accepted, persisted, and echoed with derived credit_amount; out_of_band_amount is external settlement and does not change customer cash balance. GET .../lines returns one line derived from the stored amount and memo/reason. Line-level tax, discount, PDF, and customer-balance math are outside the modeled subset.
Disputes GET /v1/disputes, GET /v1/disputes/{id}, POST /v1/disputes/{id}, POST /v1/disputes/{id}/close, GET /v1/charges/{id}/dispute, POST /v1/charges/{id}/dispute Partial Create, retrieve, list, update evidence, and close local dispute evidence for chargeback-style tests. Fixture seeding and APIs emit charge.dispute.created, charge.dispute.updated, charge.dispute.funds_withdrawn, and charge.dispute.closed where applicable. Representment deadlines, balance movements, and processor outcomes are outside the modeled subset.
Test clocks POST /v1/test_helpers/test_clocks, GET /v1/test_helpers/test_clocks, GET /v1/test_helpers/test_clocks/{id}, DELETE /v1/test_helpers/test_clocks/{id}, POST /v1/test_helpers/test_clocks/{id}/advance Partial Create/retrieve/list/advance persisted local clocks. Customers, subscriptions, and pending refunds can be attached with test_clock; advancing a clock processes attached trial activation, renewals, configured renewal failures, period-end cancellation, and refund settlement. DELETE removes the clock and detaches (rather than deletes) referenced customers and subscriptions, and re-applying a fixture pack recreates a clock that ran past the pack's declared frozen_time so the pack's premise is restored.
Webhook endpoints POST /v1/webhook_endpoints, GET /v1/webhook_endpoints, GET /v1/webhook_endpoints/{id}, POST /v1/webhook_endpoints/{id}, PATCH /v1/webhook_endpoints/{id}, DELETE /v1/webhook_endpoints/{id}, GET /v1/webhook_endpoints/{id}/attempts Supported Manage local webhook endpoints and inspect endpoint-scoped delivery attempts. Secrets are generated when omitted and masked in API responses. enabled_events supports exact event names, *, and prefix wildcards such as invoice.*. PATCH accepts the same local mutable fields as POST, including the enabled alias for active.
Events GET /v1/events, GET /v1/events/{id} Supported List and retrieve Billtap-created events. Filters include type, scenarioRunId, created[gte], created[gt], created[lte], created[lt], data.object.customer, and data.object.metadata[key].

All list and search endpoints are scoped by the selected run. Webhook endpoints registered through /runs/<runId>/v1/webhook_endpoints receive only events emitted in that run, and local test clocks are isolated the same way.

Billtap APIs

Base path: /api

Area Endpoints Scope
Checkout POST /api/checkout/sessions/{id}/complete Billtap-only checkout completion endpoint used by the hosted checkout UI and local tests.
Portal GET /api/portal, GET /api/portal/customers/{id}, POST /api/portal/subscriptions/{id}/plan-change, POST /api/portal/subscriptions/{id}/seat-change, POST /api/portal/subscriptions/{id}/cancel, POST /api/portal/subscriptions/{id}/resume, POST /api/portal/customers/{id}/payment-method Sandbox portal state and actions. These update local billing state and timeline evidence; they do not claim Stripe Billing Portal parity.
Dashboard evidence GET /api/objects, GET /api/timeline, GET /api/delivery-attempts, POST /api/debug-bundles Object lists, timelines, delivery evidence, and debug bundle data for local investigation. Timeline filters accept both object-specific IDs and generic object_type/object_id.
Webhook operations POST /api/events/{id}/replay, POST /api/events/replay-group, POST /api/webhooks/endpoints/{id}/replay-historical Replays one event or a group of events and can schedule duplicate, delayed, out-of-order, signature-mismatch, simulated endpoint response, and fail-first-then-deliver attempts. Endpoint-scoped historical replay catches up matching events that were emitted before an app registered its webhook endpoint.
Payment intent actions POST /api/payment_intents/{id}/complete_action, POST /api/payment_intents/{id}/cancel_action, POST /api/payment_intents/{id}/outcome Billtap-only callbacks used by hosted/client smoke tests to complete or abandon a local requires_action PaymentIntent, or to attach a deferred local outcome before confirming a one-time PaymentIntent.
Dispute simulation POST /api/disputes Billtap-only shortcut for creating local dispute evidence when an app does not already have a charge-like ID.
Fixtures POST /api/fixtures/apply, POST /api/fixtures/validate, GET /api/fixtures/resolve, GET /api/fixtures/snapshot, POST /api/fixtures/assert Data-driven setup and dry-run validation APIs for customers, connected accounts, products, prices, test clocks, subscription graphs, invoices, payment intents, refunds, credit notes, disputes, and timeline evidence.
Scenarios POST /api/scenarios/run Runs a scenario JSON object or YAML payload and returns the scenario report.
Boundary controls GET /api/audit-log, POST /api/retention/apply Audit and retention controls for replay, delivery overrides, and raw evidence redaction.

Billtap-only /api endpoints are also available under /runs/<runId>/api for run-scoped checkout completion, fixture apply/snapshot/assert, diagnostics, and webhook replay workflows.

Webhook Compatibility Claim

Billtap emits Stripe-style event envelopes for the supported checkout sequence and stores delivery attempts for local debugging. The envelope includes event IDs, event type, created time, livemode: false, data.object, request metadata, and Billtap metadata.

Supported generic event types:

  • checkout.session.completed
  • checkout.session.expired
  • customer.updated
  • customer.subscription.created
  • customer.subscription.updated
  • customer.subscription.deleted
  • payment_method.attached
  • invoice.created
  • invoice.finalized
  • invoice.payment_succeeded
  • invoice.paid
  • invoice.payment_failed
  • invoice.voided
  • payment_intent.created
  • payment_intent.succeeded
  • payment_intent.processing
  • payment_intent.canceled
  • payment_intent.payment_failed
  • payment_intent.requires_action
  • payment_intent.amount_capturable_updated
  • charge.refunded
  • charge.refund.updated
  • charge.dispute.created
  • charge.dispute.updated
  • charge.dispute.funds_withdrawn
  • charge.dispute.closed
  • credit_note.created
  • credit_note.voided
  • setup_intent.created
  • setup_intent.succeeded
  • setup_intent.canceled
  • setup_intent.setup_failed
  • setup_intent.requires_action
  • transfer.created
  • transfer.reversed
  • payout.created
  • payout.canceled
  • payout.reversed
  • application_fee.refunded

Current event boundaries:

  • Checkout completion emits the generic checkout, subscription, invoice, and payment-intent sequence. Async-pending checkout emits payment_intent.processing without an invoice failure event; canceled checkout emits checkout.session.expired, payment_intent.canceled, and invoice.voided.
  • Direct PaymentIntent and SetupIntent APIs emit local intent events for create, confirm, capture, cancel, failure, processing, and requires-action states. Billtap action callbacks can complete or cancel a local SCA-required intent. Cash-balance funding can settle local bank-transfer PaymentIntents for the same customer. These events are for local webhook/debug evidence and do not imply real payment processing.
  • Portal subscription actions update local billing state and timeline evidence, and enqueue customer.subscription.updated or customer.subscription.deleted when the subscription changes. Portal payment method simulation saves a deterministic default payment method and emits payment_method.attached plus customer.updated. This remains local portal evidence and does not claim Stripe Billing Portal parity.
  • Test-clock advance emits the local subscription, invoice, payment-intent, refund, credit-note, or subscription-schedule event evidence created by the processed mutation. It does not model Stripe's asynchronous clock advancement lifecycle beyond ready/advancing evidence.
  • Refund and credit-note APIs emit local payment-history events for application webhook and history screens. They do not imply balance transaction, settlement, dispute, or ledger parity.
  • Dispute APIs emit local chargeback-style evidence only. They do not imply representment, deadline, balance movement, or processor outcome parity.
  • Connect evidence APIs emit local transfer, payout, and application-fee refund events with request traces that preserve connected-account routing context. People/persons and account deletion are local evidence only. They do not imply real balance movement, onboarding, KYC, identity verification, bank verification, provider-side account closure, or settlement behavior.
  • Replay keeps the original event ID and payload, then creates new delivery attempts with replay metadata.
  • Historical endpoint replay uses POST /api/webhooks/endpoints/{id}/replay-historical, respects the endpoint's enabled_events filters, defaults until to endpoint creation time, skips already-delivered event/endpoint pairs unless force=true, and marks delivery attempts with historical replay metadata.
  • Duplicate delivery reuses the event ID and payload.
  • Delay changes delivery scheduling, not event creation time.
  • Out-of-order delivery changes attempt ordering evidence, not canonical event sequence.
  • Group replay can reorder, delay, omit, or signature-mismatch a selected set of existing event IDs so application handlers can be tested against multi-event delivery drift without changing canonical event history.

Delivery headers use:

Billtap-Signature: t=<unix_seconds>,v1=<hex_hmac_sha256>

The default header name is Billtap-specific. Set BILLTAP_WEBHOOK_SIGNATURE_HEADER=Stripe-Signature when an application already verifies Stripe's standard webhook header and should consume Billtap through the same receiver path.

Webhook envelopes emit a Stripe API version in api_version so Stripe SDK webhook deserializers can hydrate data.object into typed models. The default is 2025-12-15.clover; set BILLTAP_WEBHOOK_API_VERSION when the application under test pins a different Stripe SDK/API version.

Scenario Claim

Billtap scenarios are deterministic YAML/JSON flows for local and CI tests.

Supported generic actions:

  • customer.create
  • product.create
  • price.create
  • checkout.create
  • checkout.complete
  • checkout.cancel
  • subscription.update
  • subscription.cancel
  • subscription.resume
  • clock.advance
  • invoice.fail_payment
  • invoice.retry
  • webhook.replay
  • webhook.deliver_duplicate
  • webhook.deliver_out_of_order
  • app.assert

Supported SaaS profile actions are documented in specs/000-product/contracts/scenario.md and exercised by examples/saas-adoption-contract.yml.

Current scenario boundaries:

  • checkout.complete mutates local billing state and emits checkout-related webhook evidence.
  • checkout.cancel is a deterministic local cancellation outcome over the checkout completion path. It records an expired checkout session, a canceled payment intent, a void invoice, and checkout expiration webhook evidence.
  • subscription.update supports cancel_at_period_end for local lifecycle scenarios. subscription.cancel uses the same local portal cancellation state machine for period-end or immediate cancellation, and subscription.resume clears pending cancellation.
  • invoice.fail_payment uses the same local invoice payment mutation as invoice.retry, but defaults to a declined-card outcome when no explicit failure alias is supplied.
  • invoice.retry calls the same local invoice payment mutation as POST /v1/invoices/{id}/pay when a billing service and invoice reference are available. Success marks the invoice paid, clears next_payment_attempt, succeeds the payment intent, and reactivates the subscription. Declines keep the invoice open, increment attempt_count, set the next retry time, and update the subscription to past_due. SaaS profile-only scenarios that run without a billing invoice still record deterministic evidence only.
  • clock.advance advances scenario time and asks the local billing service to process due active/trialing subscriptions. It activates due trials, creates paid renewal invoice/payment-intent evidence, creates configured failed renewal evidence, or cancels subscriptions scheduled with cancel_at_period_end. Stripe-like test-clock APIs expose the same bounded local clock engine for fixture-backed integration tests.
  • Generic webhook replay is available through POST /api/events/{id}/replay and the webhook.replay scenario action. Replay can schedule duplicate, delayed, out-of-order, simulated endpoint status, timeout, generic transport error, signature-mismatch, and fail-first-then-deliver attempts. Endpoint catchup replay is available through POST /api/webhooks/endpoints/{id}/replay-historical for fixture events emitted before an application registered its webhook endpoint.
  • webhook.deliver_duplicate and webhook.deliver_out_of_order use generic replay delivery when they reference a Billtap event and a webhook service is configured. They still update SaaS profile webhook evidence when they reference SaaS profile events.
  • App assertions call the configured app assertion endpoint and can fail the run with a non-zero exit code.
  • Scenario reports are JSON and Markdown capable from the CLI.

Fixture Claim

Fixture packs support repeatable local setup and assertions for:

  • customers
  • catalog products
  • catalog prices
  • customer payment-method fixture controls, including payment_methods: [] and payment_methods_fixture: empty for no-card setup paths
  • customer default PaymentIntent outcome fixture controls through default_payment_intent_outcome or billtap_default_payment_intent_outcome metadata for direct and invoice-backed one-time payments
  • test clocks
  • subscription graphs created through the normal checkout-completion path
  • explicit subscription status/time fields for local lifecycle setup
  • local refund and credit-note evidence
  • optional stable checkout session, subscription, invoice, and payment intent IDs for provider-replacement tests that need exact fixture IDs
  • fixture ref resolution through GET /api/fixtures/resolve
  • seeded webhook events for fixture-created checkout, subscription, invoice, payment-intent, refund, and credit-note evidence
  • missing customer.subscription.created event backfill for pre-seeded or re-applied subscription fixtures
  • fixture-scoped snapshots
  • assertion reports for customers, products, prices, checkout sessions, subscriptions, invoices, payment intents, refunds, credit notes, and timeline entries

Fixture metadata is written to created objects:

  • billtap_fixture_name
  • billtap_fixture_run_id
  • billtap_fixture_namespace
  • billtap_fixture_ref

Fixtures are intended for local and CI setup. They are not migration tooling and must not contain real card data, live credentials, or production customer data.

Adoption Smoke Claim

npm run smoke:sdk exercises the documented Stripe-like subset with the official stripe-node SDK. The lane covers customer, product, price, checkout session, event, webhook endpoint, and related retrieve/list flows against an isolated local Billtap server by default. It can target an existing Billtap server with BILLTAP_STRIPE_SDK_SMOKE_BASE_URL.

Unsupported Stripe Behavior

Billtap does not support or claim:

  • Full Stripe API coverage.
  • Real payment processing or live payment success paths.
  • Stripe Dashboard behavior.
  • Real card data, PAN, CVC, expiration fields, or live credentials.
  • Full Stripe request idempotency-key semantics across all endpoints. Billtap only caches same-process POST responses for supported API simulation and rejects same-key parameter mismatches.
  • Direct charge creation, balance transactions, external-account verification, real Connect onboarding, real payouts/transfers settlement, mandates, tax, full discount redemption, full subscription-schedule billing, quotes, or usage-based metering.
  • Full refund and credit-note ledger behavior, including balance transactions, settlement, failed refund processing, line tax allocation, PDF rendering, and provider accounting side effects.
  • PaymentIntent customer-balance application, incremental authorization, bank microdeposit verification, and full payment-method option parity.
  • Stripe-hosted Checkout or Billing Portal parity.
  • Provider-specific settlement, risk, tax, invoice rendering, fraud, account, payout, or dispute behavior.
  • Complete webhook event coverage.
  • Full invoice rendering, send, void, line mutation, automatic collection, tax, and dunning automation. Billtap supports the checkout retry subset and the bounded manual one-time invoice flow documented above.

Use Stripe testmode or the real provider sandbox as the fallback lane for these behaviors.

Compatibility Change Rules

Before adding a new compatibility claim:

  1. Add or update a fixture, scenario, or contract test.
  2. Document the endpoint, event, action, or boundary here.
  3. State unsupported provider behavior instead of silently approximating it.
  4. Run the release verification in docs/RELEASE.md.