---
title: "Create checkout (server-side start-checkout)"
method: POST
path: "/api/v2/billing/checkout"
tags: ["v2 Billing"]
---

# Create checkout (server-side start-checkout)

`POST /api/v2/billing/checkout`

## Request

- Base URL: `https://pdf2md.dev`
- URL: `https://pdf2md.dev/api/v2/billing/checkout`
- Auth: one of:
  - API key in cookie `p2m_session`
  - none

EP-023-T8 (implemented). Session OR anonymous+email (paywall without login; anonymous → claimable account created_via=billing). The server calls monetize start-checkout with our x-api-key, userMeta.p2m_account_id, success/error/shop URLs on our domain. Returns checkout_url to open in a tab; the provider userId is saved as monetize_customer_id. EP-065-T3: checkout creation is serialized per account (a short lock). A repeat of the SAME plan while one is in flight replays the same checkout_url; a DIFFERENT plan gets 409 checkout_in_progress. Without the lock two concurrent requests both bypassed the provider's active-purchase protection, which is a double charge. EP-062-T4: whether the provider's active-purchase protection is bypassed (ignoreActivePurchase) is decided SERVER-SIDE by comparing the requested price with the account's stored subscription — tier ranks, then billing interval (month &lt; year &lt; lifetime). Only a proven upgrade bypasses it; the same plan, a lower tier, a shorter interval or an unknown stored interval get 409 active_subscription with a message directing the buyer to the billing portal (starting a second checkout would charge them twice). EP-066-T1: the bypass requires an AUTHENTICATED caller (session or device-signed intent_token). An anonymous email checkout never receives it — the account was resolved from a string in the request body, so nothing proves the caller owns that address — and it therefore never takes the per-account lock either. Consequently checkout_in_progress can only be caused by the account's own signed-in requests. EP-066-T7: a subscription that is CANCELLED and merely running out its paid period is a re-subscribe, not a repeat purchase: the checkout is allowed (without the bypass) instead of answering 409 same_plan. `trialing` is not included — a trial converts at the provider.

## Request body

- V2CheckoutRequest: Specify price_id OR (tier_code+interval). email is required only for the anonymous flow.
  - `price_id` string
  - `tier_code` 'anonymous' | 'free' | 'lifetime' | 'builder' | 'pro' | 'business' | 'enterprise': EP-069: `lifetime` is the one-time unlock, ranked BETWEEN free and builder — it raises the interactive limits and carries 600 pages/month, but grants no API keys, hosted MCP, webhooks or batch, so a subscription is genuinely more and its holder may still buy one.
  - `interval` 'month' | 'year'
  - `email` string, email: for anonymous checkout (if there is no session)
  - `channel` 'extension' | 'web': Checkout source for runtime purchase flags (EP-023-T23). Treat absence as web for site/pricing or by the calling client.
  - `trial_days` integer, nullable
  - `upgrade` boolean: IGNORED since EP-062-T4 (the server decides from the stored subscription whether ignoreActivePurchase applies). Still accepted so already-published extension builds keep working.
  - `intent_token` string: EP-028: device-signed checkout-intent token (from POST /billing/checkout-intent). When valid it names the account to charge (the extension's account), overriding the web session. One-time consume; invalid/expired falls back to session/email.

## Response `200`

Checkout created

- V2CheckoutResponse
  - `checkout_url` string, uri, required: open in a new tab (hosted by Stripe/Paddle)

## Other responses

- `400`: Email required (email_required) or unknown plan
- `409`: active_subscription — direct to portal (carries `reason` for a SIGNED-IN buyer only, EP-065-T8) | checkout_in_progress — another checkout for this account is being created (EP-065-T3)
- `429`: Rate limit exceeded
- `503`: provider_unavailable — the provider could not create a checkout (it errored, was unreachable, or answered 200 with no url). Nothing was charged; the per-account lock is released, so an immediate retry is safe. EP-076-T1: this used to be 502 validation_error, i.e. the SAME code as an unknown plan or a malformed body — no surface could tell an outage from a bad request, so every one of them printed "Could not start checkout. Please try again.", which blames the buyer and recommends the one thing that cannot work. Clients localize from the code; the English message is the fallback. | billing_not_configured — this deployment sells nothing (no MONETIZE_API_KEY/PAYWALL_ID/PUBLIC_BASE_URL).

---

[API](https://skmtc.dev/pdf2md/apis/pdf-to-markdown-api-facade.md) · [All operations](https://skmtc.dev/pdf2md/apis/pdf-to-markdown-api-facade/llms.txt) · [OpenAPI document](https://skmtc.dev/pdf2md/apis/pdf-to-markdown-api-facade/revisions/4636782fb8b6?raw)
