v2 Billing

Webhook from monetize.software (server→server)

EP-023-T14 (implemented). Called by monetize, NOT by the extension. Authentication — the X-Monetize-Signature header (HMAC-SHA256 of the raw body, secret MONETIZE_WEBHOOK_SECRET, timing-safe, optional sha256= prefix). Idempotency by event.id (billing_events; duplicate→200 no-op), anti out-of-order by last_event_at. Account resolution: userMeta.p2m_account_id → otherwise customer.email. subscription.created/updated/cancelled + payment.completed + refund.created → subscriptions (status+tier from price_id→billing_price_map). 5xx on a transient DB error (the provider retries). See v2.0.0-billing-monetize.md §5. Scheduled downgrade — maintenance. EP-063-T1/EP-064-T2: webhooks are ACCOUNT-scoped at monetize, so events of another paywall arrive here too. An event naming a different paywall is ignored (200, no DB access); an event naming NO paywall is applied only when the product is proven independently — our userMeta.p2m_account_id, a price in billing_price_map, or a provider_subscription_id we already store — otherwise 200 + ALERT without any write. EP-064-T1: an event whose created_at does not parse is NOT applied (200, ALERT) — an unorderable event would otherwise outrank every stored state; the sole exception is an explicit revocation, which is applied with the delivery time. EP-065-T2: paywall ids are compared as numbers whenever both denote a whole number, in ANY spelling (827, 0827, +827, 827.0, 8.27e2); a fractional id denotes no whole paywall and stays foreign. EP-065-T6: proof of product is also required from events that carry MONEY (payment.completed, refund.created) — billing_events is what revenue is reconstructed from, so a foreign payment recorded there is counted as our sale. EP-069-T9: payment.completed also GRANTS the one-time lifetime unlock, so it is now an entitlement-changing event. Which kind it is (a lifetime purchase vs the first charge of a subscription) is decided by looking data.price.id up in billing_price_map — never by the event's own price.interval, because both arrive as the same type and branching on the event field races subscription.created for the same account. An unmapped price grants nothing. EP-065-T4: a repository error while checking ownership is a TRANSIENT failure (5xx, the provider retries), never a terminal 200 — the two used to be indistinguishable and silently dropped paid sales. EP-066-T2: the same rule now covers ACCOUNT RESOLUTION. A database failure while looking up p2m_account_id or creating the claimable account by email answers 5xx BEFORE the event is claimed; only a successful lookup that named nobody gives 200 skipped_no_account. EP-066-T3: an empty MONETIZE_PAYWALL_ID answers 503 (with an ALERT) instead of applying events blind — without the id every cross-paywall guard degrades to "allow".

post/api/v2/billing/webhook

Request

  • Base URL: https://pdf2md.dev
  • URL: https://pdf2md.dev/api/v2/billing/webhook
  • Auth: none required

Headers

X-Monetize-Signaturestring required

sha256=<hmac> of the raw body (see security.md)

Request body

object required

monetize event {id,type,created_at,data}

Response

Accepted/processed. status: processed | duplicate | skipped_no_account | ignored_foreign_paywall | ignored_unproven_paywall | skipped_unorderable_event

Changes

No recorded changes to this operation across all 1 revision of this API.