{"result":{"depiction_url":null,"edges":{"in_module":{"objects":[{"created":"2026-10-22T10:28:13Z","object_id":{"id":3105,"is_a":["text","documentation","reference","module"],"name":"doc_external_9_mod_paysub","title":"mod_paysub","uri":"https:\/\/zotonic.com\/id\/3105"},"seq":1}],"predicate":{"id":333,"is_a":["meta","predicate"],"name":"in_module","title":{"_type":"trans","tr":{"en":"In module"}},"uri":"https:\/\/zotonic.com\/id\/in_module"}},"subject":{"objects":[{"created":"2026-10-22T10:28:13Z","object_id":{"id":2555,"is_a":["categorization","keyword","keyword_information_type"],"name":"zotonic_topic_reference","title":"Reference","uri":"https:\/\/zotonic.com\/id\/2555"},"seq":1},{"created":"2026-10-22T10:28:13Z","object_id":{"id":2564,"is_a":["categorization","keyword","keyword_audience"],"name":"zotonic_topic_backend_developer","title":"Backend developer","uri":"https:\/\/zotonic.com\/id\/2564"},"seq":2},{"created":"2026-10-22T10:28:13Z","object_id":{"id":2565,"is_a":["categorization","keyword","keyword_audience"],"name":"zotonic_topic_integrator","title":"Integrator","uri":"https:\/\/zotonic.com\/id\/2565"},"seq":3},{"created":"2026-10-22T10:28:13Z","object_id":{"id":2599,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_api_and_integration","title":"API and integration","uri":"https:\/\/zotonic.com\/id\/2599"},"seq":4},{"created":"2026-10-22T10:28:13Z","object_id":{"id":2626,"is_a":["categorization","keyword","keyword_architecture"],"name":"zotonic_topic_controller","title":"Controller","uri":"https:\/\/zotonic.com\/id\/2626"},"seq":5},{"created":"2026-10-22T10:28:13Z","object_id":{"id":2674,"is_a":["categorization","keyword","keyword_technology"],"name":"zotonic_topic_json","title":"JSON","uri":"https:\/\/zotonic.com\/id\/2674"},"seq":6},{"created":"2026-10-22T10:28:13Z","object_id":{"id":2676,"is_a":["categorization","keyword","keyword_technology"],"name":"zotonic_topic_http","title":"HTTP","uri":"https:\/\/zotonic.com\/id\/2676"},"seq":7}],"predicate":{"id":308,"is_a":["meta","predicate"],"name":"subject","title":{"_type":"trans","tr":{"en":"Keyword"}},"uri":"http:\/\/purl.org\/dc\/elements\/1.1\/subject"}}},"id":3106,"is_a":["text","documentation","reference","controller"],"links":[{"rel":"self","target":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/3106"},{"rel":"hub","target":"https:\/\/zotonic.com\/.zotonic\/websub"}],"medium":null,"medium_url":null,"name":"doc_external_9_controller_paysub_stripe_webhook","page_url":{"en":"https:\/\/zotonic.com\/docs\/3106\/controller_paysub_stripe_webhook","x-default":"https:\/\/zotonic.com\/docs\/3106\/controller_paysub_stripe_webhook"},"preview_url":null,"resource":{"version":17,"pivot_location_lat":null,"title":"controller_paysub_stripe_webhook","is_authoritative":true,"body":"<p>Receive Stripe payment and subscription events with <code>POST<\/code> at\n<code>\/paysub\/stripe\/webhook<\/code> (dispatch <code>paysub_stripe_webhook<\/code>). Generate the\nabsolute endpoint URL in a template with:<\/p>\n<pre class=\"notranslate\"><code class=\"notranslate language-django\">{% url paysub_stripe_webhook absolute_url %}\n<\/code><\/pre>\n<h2>Authentication<\/h2>\n<p>Set the per-site <code>mod_paysub.stripe_webhook_secret<\/code> to the endpoint signing\nkey. Requests need a <code>stripe-signature<\/code> header containing <code>t<\/code> and <code>v1<\/code>.\nThe controller compares <code>v1<\/code> to the lowercase hexadecimal HMAC-SHA256 of\n<code>timestamp.raw_body<\/code>, and requires the timestamp to differ from server time\nby less than 300 seconds. It uses the raw body before JSON decoding.\nThere is no user-session ACL check. Missing or invalid signatures fail\nauthorization; the timestamp window does not provide event-ID deduplication.<\/p>\n<h2>Events<\/h2>\n<p>The JSON body must contain <code>type<\/code> and the event-specific <code>data.object<\/code>.<\/p>\n<table class=\"table\"><thead><tr><th>Event family<\/th><th>Handling<\/th><\/tr><\/thead><tbody><tr><td><code>checkout.session.completed<\/code><\/td><td>Complete the local checkout through the Stripe adapter.<\/td><\/tr><tr><td><code>checkout.session.async_payment_failed<\/code>, <code>.async_payment_succeeded<\/code>, <code>.expired<\/code><\/td><td>Synchronize the checkout session.<\/td><\/tr><tr><td><code>invoice.created<\/code>, <code>.updated<\/code>, <code>.voided<\/code>, <code>.finalized<\/code>, <code>.paid<\/code>, <code>.payment_failed<\/code><\/td><td>Synchronize the invoice.<\/td><\/tr><tr><td><code>invoice.deleted<\/code><\/td><td>Delete the local invoice.<\/td><\/tr><tr><td><code>customer.created<\/code>, <code>.updated<\/code>, <code>.deleted<\/code><\/td><td>Synchronize or delete the local customer.<\/td><\/tr><tr><td><code>customer.subscription.created<\/code>, <code>.updated<\/code>, <code>.deleted<\/code><\/td><td>Synchronize the subscription.<\/td><\/tr><tr><td><code>price.*<\/code>, <code>product.*<\/code><\/td><td>Schedule price or product synchronization.<\/td><\/tr><tr><td><code>payment_intent.*<\/code><\/td><td>Synchronize the payment.<\/td><\/tr><tr><td><code>ping<\/code><\/td><td>Acknowledge without synchronization.<\/td><\/tr><\/tbody><\/table>\n<p>Other events are acknowledged without updating records. Some synchronization\nis queued, so acknowledgement does not imply that every local record has\nalready been refreshed. Handler results <code>ok<\/code> and <code>{ok, _}<\/code> acknowledge the\nrequest; <code>{error, _}<\/code> produces HTTP 500. Malformed JSON or missing required\nevent fields are not converted into a structured validation response.<\/p>","slug":"controller_paysub_stripe_webhook","doc_module_config":[],"is_protected":false,"visible_for":0,"tz":"UTC","language":["en"],"doc_source_hash":"45b8d927e96286b8afe7e28c997d770a5418aefd57ab5f74fe42c6247b61b31b","is_featured":false,"content_group_id":{"id":2551,"is_a":["meta","content_group"],"name":"content_group_imported_docs","title":"Imported documentation","uri":"https:\/\/zotonic.com\/id\/content_group_imported_docs"},"external_module_id":{"id":9,"is_a":[],"name":null,"title":null,"uri":"https:\/\/zotonic.com\/id\/9"},"git_url":"https:\/\/github.com\/zotonic\/zotonic_mod_paysub.git","category_id":{"id":321,"is_a":["meta","category"],"name":"controller","title":"Controllers","uri":"https:\/\/test.zotonic.com\/id\/321"},"doc_source_path":".\/src\/controllers\/controller_paysub_stripe_webhook.erl","publication_start":"2026-10-22T10:28:13Z","github_url":"https:\/\/github.com\/zotonic\/zotonic_mod_paysub.git","pivot_location_lng":null,"doc_source_kind":"external","name":"doc_external_9_controller_paysub_stripe_webhook","is_unfindable":false,"is_published":true,"pivot_geocode":null,"external_module_title":"Paid Subscriptions","created":"2026-10-22T10:28:13Z","uri":null,"doc_status":"current","is_dependent":false,"is_external_module":true,"publication_end":"9999-06-01T00:00:00Z","modifier_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"privacy":0,"doc_source_commit":"3a4ee4d7434205778e4318c0c2e1576884e5678b","creator_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"modified":"2026-10-22T10:28:14Z","title_slug":"controller_paysub_stripe_webhook"},"uri":"https:\/\/zotonic.com\/id\/3106","uri_template":"https:\/\/zotonic.com\/id\/:id","websub":{"hub":"https:\/\/zotonic.com\/.zotonic\/websub","topic":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/3106"}},"status":"ok"}