---
title: "Webhook from monetize.software (server→server)"
method: POST
path: "/api/v2/billing/webhook"
tags: ["v2 Billing"]
---

# Webhook from monetize.software (server→server)

`POST /api/v2/billing/webhook`

## Request

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

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".

## Headers

- `X-Monetize-Signature` string, required

## Request body

- object: monetize event {id,type,created_at,data}

## Response `200`

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

## Other responses

- `401`: Invalid signature
- `429`: Rate limit exceeded
- `500`: Transient error — the provider must retry. Includes 'we could not CHECK whether the event is ours' (EP-065-T4) and 'we could not RESOLVE the account' (EP-066-T2); both are deliberately NOT answered with 200.
- `503`: Webhook not configured: MONETIZE_WEBHOOK_SECRET or MONETIZE_PAYWALL_ID missing (EP-066-T3). Without the paywall id the cross-paywall guard cannot work, so events are refused rather than applied blind; the provider retries.

---

[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)
