{"result":{"depiction_url":null,"edges":{"in_module":{"objects":[{"created":"2026-09-29T13:07:54Z","object_id":{"id":2703,"is_a":["text","documentation","reference","module"],"name":"doc_module_mod_websub","title":"mod_websub","uri":"https:\/\/zotonic.com\/id\/2703"},"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-09-29T13:07:54Z","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-09-29T13:07:54Z","object_id":{"id":2565,"is_a":["categorization","keyword","keyword_audience"],"name":"zotonic_topic_integrator","title":"Integrator","uri":"https:\/\/zotonic.com\/id\/2565"},"seq":2},{"created":"2026-09-29T13:07:54Z","object_id":{"id":2592,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_authorization_and_access_control","title":"Authorization and access control","uri":"https:\/\/zotonic.com\/id\/2592"},"seq":3},{"created":"2026-09-29T13:07:54Z","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-09-29T13:07:54Z","object_id":{"id":2601,"is_a":["categorization","keyword","keyword_domain"],"name":"zotonic_topic_export_and_syndication","title":"Export and syndication","uri":"https:\/\/zotonic.com\/id\/2601"},"seq":5},{"created":"2026-09-29T13:07:54Z","object_id":{"id":2625,"is_a":["categorization","keyword","keyword_architecture"],"name":"zotonic_topic_model","title":"Model","uri":"https:\/\/zotonic.com\/id\/2625"},"seq":6},{"created":"2026-09-29T13:07:54Z","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":2712,"is_a":["text","documentation","reference","model"],"links":[{"rel":"self","target":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/2712"},{"rel":"hub","target":"https:\/\/zotonic.com\/.zotonic\/websub"}],"medium":null,"medium_url":null,"name":"doc_model_model_websub","page_url":{"en":"https:\/\/zotonic.com\/docs\/2712\/websub","x-default":"https:\/\/zotonic.com\/docs\/2712\/websub"},"preview_url":null,"resource":{"version":19,"pivot_location_lat":null,"title":"websub","is_authoritative":true,"body":"<p>Model for WebSub resource subscriptions, delivery queues, and automatic imports.<\/p>\n<h2>Available model API paths<\/h2>\n<table class=\"table\"><thead><tr><th>Method<\/th><th>Path<\/th><th>Result<\/th><\/tr><\/thead><tbody><tr><td><code>get<\/code><\/td><td><code>\/subscriptions<\/code><\/td><td>Paginated overview; requires <code>use mod_websub<\/code>. Payload: <code>type<\/code> (<code>all<\/code>, <code>export<\/code>, <code>import<\/code>), numeric local <code>rsc_id<\/code>, <code>hostname<\/code>, <code>status<\/code>, <code>errors<\/code> (<code>yes<\/code>\/<code>no<\/code>\/<code>all<\/code>), and <code>page<\/code>.<\/td><\/tr><tr><td><code>get<\/code><\/td><td><code>\/subscriber_count\/+id<\/code><\/td><td>Number of unexpired incoming subscriptions; requires resource edit permission.<\/td><\/tr><tr><td><code>get<\/code><\/td><td><code>\/status\/+id<\/code><\/td><td>Latest import subscription status for an editable resource, or <code>undefined<\/code> when none exists.<\/td><\/tr><\/tbody><\/table>\n<p><code>+id<\/code> is resolved through <code>m_rsc:rid\/2<\/code>. The template equivalent is\n<code>m.websub.status[id]<\/code>; the model checks resource edit permission independently of\nthe template or controller. Unauthorized reads return <code>eacces<\/code>. There are no\npublic model POST or DELETE paths for starting or stopping subscriptions.<\/p>\n<p>The overview includes retained expired\/stopped subscriptions and renewal generations,\n50 rows per page. Hostname is an exact, case-insensitive match on the incoming\ncallback host or outgoing source host, ignoring ports and a trailing DNS dot.\nStatus accepts <code>active<\/code>, <code>pending<\/code>, <code>expired<\/code>, or <code>stopped<\/code>; empty\/<code>all<\/code> means all\nstates. The errors filter selects rows with or without the table’s error indicator.\nAll filters apply before pagination and counting. The overview returns a\n<code>search_result<\/code> record with an exact total, capped at 10,000 pages.\nIt returns remote hostnames, never callback URLs, tokens, secrets,\nor authentication data. Invalid filters return <code>is_invalid<\/code> without broadening the query.<\/p>\n<p>Status includes desired state (<code>is_enabled<\/code>), confirmation state\n(<code>is_unsubscribed<\/code>, <code>is_active<\/code>), <code>pending_mode<\/code>, <code>lease<\/code>, <code>last_received<\/code>,\n<code>last_error<\/code>, and <code>credential_error<\/code>. During renewal, <code>is_active<\/code> also accounts\nfor a still-active predecessor callback. Status never exposes callback tokens,\nHMAC secrets, or source OAuth2 tokens.<\/p>\n<h2>Erlang API<\/h2>\n<ul><li><code>subscribe\/2<\/code> starts a durable subscription for an editable, non-authoritative\nresource with a remote URI. It requires a logged-in user and is idempotent for\nan already-enabled subscription to that source. Discovery and verification run\nasynchronously; <code>ok<\/code> means intent was recorded, not that the hub confirmed it.<\/li><li><code>unsubscribe\/2<\/code> requires edit permission, immediately disables automatic imports,\ndiscards queued updates, and schedules remote unsubscription.<\/li><li><code>topic_url\/2<\/code> generates the language-independent JSON topic URL. Use\n<code>m_rsc:uri\/2<\/code> for the resource&#39;s semantic <code>\/id<\/code> identity instead.<\/li><\/ul>\n<pre class=\"notranslate\"><code class=\"notranslate language-erlang\">ok = m_websub:subscribe(Id, Context).\nok = m_websub:unsubscribe(Id, Context).\n<\/code><\/pre>\n<p>Admin postbacks call these checked functions. The remaining exported queue,\nverification, export-registration, schema, and maintenance functions are internal\nintegration APIs. They are not exposed through model paths. In particular,\n<code>update_export\/6<\/code> and <code>delete_export\/3<\/code> are called after the hub has verified intent;\ncallers must not use them to bypass authorization or callback verification.<\/p>\n<h2>Delivery and import processing<\/h2>\n<p>Publisher queues coalesce resource versions, enforce lease expiry, and recheck\ncurrent access within the original authentication&#39;s group restrictions. Delivery\ncontains the complete JSON topic representation and a signature when a secret was\nsupplied. Exhausted retries discard that notification, allowing later updates to\nretry delivery for the remaining lease.<\/p>\n<p>Subscriber callbacks identify the subscription by capability token and validate\nthe signed resource URI. Public imports consume the pushed JSON; credentialed\nimports refetch through <code>z_fetch<\/code> using the subscribing user&#39;s source credentials.\nBefore applying content, the import transaction checks current edit permission,\nnon-authoritative status, source identity, and version ordering. Saved import\noptions are reused without allowing automatic updates to restart a stopped\nsubscription. Resource identity is stored separately from the discovered WebSub\n<code>topic_url<\/code>, so topic migration does not change the semantic resource identifier.<\/p>\n<p>See <code>mod_websub<\/code> for the two-site protocol flow and <code>z_websub_subscription<\/code> for\nrenewal, pending intent, callback rotation, and subscriber state persistence.<\/p>","slug":"websub","is_protected":false,"visible_for":0,"tz":"UTC","language":["en"],"doc_source_hash":"af334f4f1958aed9fd1a24417229ddd9a5ee6aac6c5c8bb95fea4fbd04ec059e","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"},"category_id":{"id":322,"is_a":["meta","category"],"name":"model","title":"Models","uri":"https:\/\/test.zotonic.com\/id\/322"},"doc_source_path":"apps\/zotonic_mod_websub\/src\/models\/m_websub.erl","publication_start":"2026-09-29T13:07:54Z","github_url":"https:\/\/github.com\/zotonic\/zotonic\/blob\/master\/apps\/zotonic_mod_websub\/src\/models\/m_websub.erl","pivot_location_lng":null,"doc_source_kind":"model","name":"doc_model_model_websub","is_unfindable":false,"is_published":true,"pivot_geocode":null,"created":"2026-09-29T13:07:54Z","uri":null,"doc_status":"current","is_dependent":false,"erlang_app":"zotonic_mod_websub","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":"85498256abd162b9bc082d43a90c995844fd4408\n","erlang_module":"m_websub","creator_id":{"id":1,"is_a":["person"],"name":"administrator","title":"Site Administrator","uri":"https:\/\/zotonic.com\/id\/1"},"modified":"2026-09-29T14:40:25Z","title_slug":"websub"},"uri":"https:\/\/zotonic.com\/id\/2712","uri_template":"https:\/\/zotonic.com\/id\/:id","websub":{"hub":"https:\/\/zotonic.com\/.zotonic\/websub","topic":"https:\/\/zotonic.com\/.zotonic\/websub\/topic\/2712"}},"status":"ok"}