{"id":"linked-art/activity-stream-partner-quickstart","relativePath":"linked-art/activity-stream-partner-quickstart.md","title":"ActivityStreams Partner Quickstart","markdown":"# ActivityStreams Partner Quickstart\n\nLast updated: 2026-07-07\n\nThis quickstart helps an external partner read the MetaMuseum ActivityStreams feed. It is onboarding documentation only. It does not count as external adoption evidence until a real partner-owned consumer ID, public feed read, durable HTTPS subscription, recent 2xx callback verification, and refreshed syndication evidence packet are recorded.\n\n## What To Ask For\n\nAsk the partner for:\n\n- A stable partner-owned consumer ID, such as `museum-reader-prod`.\n- The system or organization that owns that consumer ID.\n- A public HTTPS callback URL if they can receive activity notifications.\n- A contact who can confirm observed activity types and callback results.\n- A real evidence reference, such as an issue, ticket, log export, or email thread.\n\nDo not use demo, fixture, local, placeholder, sample, smoke, test, localhost, private-network, or MetaMuseum-owned IDs as strict evidence.\n\n## Read The Feed\n\nUse the partner-owned ID in `x-linked-art-consumer-id`.\n\n```bash\ncurl \"https://www.metamuseum.org/api/activity?limit=100\" \\\n  -H \"accept: application/activity+json\" \\\n  -H \"x-linked-art-consumer-id: museum-reader-prod\"\n```\n\nFor strict adoption evidence, read each required activity type with the same\ndeclared header as well. If a type-filtered URL returns zero items, report that\nexplicitly so operators can distinguish missing feed content from missing\npartner coverage.\n\n```bash\ncurl \"https://www.metamuseum.org/api/activity?limit=100&type=Create\" \\\n  -H \"accept: application/activity+json\" \\\n  -H \"x-linked-art-consumer-id: museum-reader-prod\"\n\ncurl \"https://www.metamuseum.org/api/activity?limit=100&type=Update\" \\\n  -H \"accept: application/activity+json\" \\\n  -H \"x-linked-art-consumer-id: museum-reader-prod\"\n\ncurl \"https://www.metamuseum.org/api/activity?limit=100&type=Delete\" \\\n  -H \"accept: application/activity+json\" \\\n  -H \"x-linked-art-consumer-id: museum-reader-prod\"\n```\n\nThe response is an ActivityStreams `OrderedCollectionPage`. Each item includes:\n\n- `type`: `Create`, `Update`, or `Delete`\n- `object.id`: affected dereferenceable MetaMuseum Linked Art object URI\n- `object.sourceId`: original provider/stored object URI when it differs from `object.id`\n- `object.activityHref`: object-scoped feed URL\n- `object.equivalent[]`: reconciliation hooks such as provider URIs or Wikidata QIDs when available\n- `linkedArt`: embedded Linked Art JSON-LD projection; `linkedArt.id` is a dereferenceable `/api/events/{encodedSourceActivityId}` URL, embedded projections carry `_complete: false`, and `linkedArt.equivalent[]` preserves the source activity URI when available\n- `source.kind`: `audit`, `record-backfill`, `record-semantic`, or `deletion`\n- `endTime`: valid UTC sync timestamp; original placeholder times are retained under `extensions.sourceEndTime`\n- `nextCursor`: stable cursor for incremental sync\n\nDefault cursor-feed reads collapse record-backed diagnostic rows to the latest\nharvestable public row per object, matching the IIIF Change Discovery projection.\nUse `source=record-semantic` or another `source=` filter when you need the\nunderlying semantic diagnostic rows instead of the deduped partner view.\n\n## Verify Update Linked Art Event Dereference\n\nWhen `type=Update` returns at least one item, fetch the first row's\n`orderedItems[0].linkedArt.id`. The response should return HTTP `200` with the\nLinked Art JSON-LD context, `entity.id` equal to the requested URL,\n`entity._complete: false`, and `entity.equivalent[]` preserving the source\nactivity URI.\n\n```bash\ncurl \"https://www.metamuseum.org/api/activity?limit=100&type=Update\" \\\n  -H \"accept: application/activity+json\" \\\n  -H \"x-linked-art-consumer-id: museum-reader-prod\"\n\ncurl \"<orderedItems[0].linkedArt.id>\" \\\n  -H \"accept: application/ld+json\" \\\n  -H \"x-linked-art-consumer-id: museum-reader-prod\"\n```\n\nIf `type=Update` is zero, report zero and skip this dereference check. If\n`type=Delete` is zero, report zero explicitly as well; MetaMuseum does not emit\nDelete rows until a genuine upstream `404` or `410` tombstone exists.\n\n## Cursor Sync\n\nRead the first page, store `nextCursor`, then request the next page with `cursor`.\n\n```bash\ncurl \"https://www.metamuseum.org/api/activity?limit=100&cursor=<nextCursor>\" \\\n  -H \"accept: application/activity+json\" \\\n  -H \"x-linked-art-consumer-id: museum-reader-prod\"\n```\n\nUseful filters:\n\n- `type=Create`, `type=Update`, or `type=Delete`\n- `source=record-backfill`, `source=record-semantic`, `source=audit`, or `source=deletion`\n- `provider=met`, `provider=vanda`, `provider=museumsvictoria`, or another provider key\n- `object=<encoded object URI>`\n- `since=<ISO timestamp>`\n- `until=<ISO timestamp>`\n\nWhen `limit` is absent, the feed returns up to `25` items. Explicit limits are clamped to `1..100`. The canonical `next` link uses a cursor only when the page boundary has a valid timestamp; otherwise it falls back to the offset `nextPage` link instead of emitting a bad cursor.\n\n## IIIF Walk-Back Harvest\n\nFor IIIF Change Discovery-compatible consumers, discover the fixed-page collection first:\n\n```bash\ncurl \"https://www.metamuseum.org/api/activity/collection?limit=100\" \\\n  -H \"accept: application/activity+json\" \\\n  -H \"x-linked-art-consumer-id: museum-reader-prod\"\n```\n\nStart from `last.id`, process that page in reverse item order if your mirror wants newest-first application, then follow each page's `prev.id` until you reach a timestamp older than your last crawl. Send `x-linked-art-consumer-id` on the collection request and every page request; MetaMuseum records those walk-back reads in the same readiness ledger as cursor-feed reads, and page reads record the concrete activity types observed on that page. The page documents themselves list activities oldest-to-newest so the most recent activity on a page appears last, matching the IIIF Change Discovery API 1.0 consumption model. The same filters used by cursor sync, including `provider=`, are preserved in `first`, `last`, `prev`, and `next` links.\n\nDiscovery pages are thin by default: `linkedArt` is omitted, `endTime` is always a valid UTC timestamp, `object.equivalent[]` is present when stored records carry reconciliation hooks, and bare stored IDs are projected as dereferenceable `/api/records/{id}` object URLs with the original value preserved as `object.sourceId`. The cursor feed uses the same object/time projection while keeping `linkedArt` embedded. To include the richer Linked Art projection during traversal, add `embed=linked-art`:\n\n```bash\ncurl \"https://www.metamuseum.org/api/activity/collection?limit=100&embed=linked-art\" \\\n  -H \"accept: application/activity+json\" \\\n  -H \"x-linked-art-consumer-id: museum-reader-prod\"\n```\n\n## Object Feed\n\nFor one object, use the encoded object URI:\n\n```bash\ncurl \"https://www.metamuseum.org/api/activity/object/https%3A%2F%2Fexample.org%2Fobject%2F123?limit=100\" \\\n  -H \"accept: application/activity+json\" \\\n  -H \"x-linked-art-consumer-id: museum-reader-prod\"\n```\n\nRecord detail responses also advertise activity discovery with HTTP `Link`\nheaders. A client that dereferences `/api/records/{id}` can find:\n\n- `rel=\"http://iiif.io/api/discovery/1#activity\"` for `/api/activity/collection`\n- `rel=\"alternate\"` for the cursor feed and object-scoped activity feed\n\nThe same activity metadata is listed under `/api/providers/capabilities` in\n`activitySyndication`.\n\n## Register A Callback\n\nIf the partner can receive events, register an HTTPS callback:\n\n```bash\ncurl -X POST \"https://www.metamuseum.org/api/activity/subscriptions\" \\\n  -H \"content-type: application/json\" \\\n  -H \"accept: application/activity+json\" \\\n  -H \"x-linked-art-consumer-id: museum-reader-prod\" \\\n  --data '{\"callbackUrl\":\"https://partner.example.org/metamuseum/activity\",\"eventTypes\":[\"Create\",\"Update\",\"Delete\"],\"signingRequired\":true,\"signingSecretRef\":\"museum-reader-prod-v1\"}'\n```\n\nThe callback URL must be public HTTPS and owned by the partner. When signing is enabled, the subscription stores only `signingSecretRef`; the actual HMAC secret is exchanged out of band. Callback deliveries are signed over `<timestamp>.<raw JSON payload>` with HMAC-SHA256 and include:\n\n- `x-metamuseum-signature`: `sha256=<hex digest>`\n- `x-metamuseum-signature-timestamp`: UTC signing timestamp\n\nA subscription row alone is not enough for strict evidence; it also needs recent 2xx verification.\n\n## Verify Locally After Partner Reads\n\nAfter a real partner reads the feed:\n\n```powershell\npnpm activity:adoption:matrix -- --base-url https://www.metamuseum.org --consumer-id museum-reader-prod\npnpm activity:subscription:verify -- --consumer-id museum-reader-prod --callback-url https://partner.example.org/metamuseum/activity --status-code 204 --verified-at <ISO timestamp> --evidence-ref <real-proof-ref>\npnpm activity:syndication:evidence\n```\n\nThe evidence packet remains blocked until `consumerOnboarding.rows` shows the same partner consumer has feed-read telemetry, Create/Update/Delete type coverage, a durable subscription, callback verification, and strict packet refresh proof.\n\n## Partner Response Template\n\n```text\nConsumer ID:\nOrganization/system:\nFeed read timestamp:\nType-specific read timestamps:\nObserved activity types:\nUpdate linkedArt.id dereference:\nUpdate linkedArt.id notes:\nObject feed tested:\nCallback URL:\nCallback event types:\nCallback verification status code:\nCallback verification timestamp:\nEvidence ref:\nContact:\n```\n","sections":[{"level":2,"heading":"What To Ask For","anchor":"what-to-ask-for"},{"level":2,"heading":"Read The Feed","anchor":"read-the-feed"},{"level":2,"heading":"Verify Update Linked Art Event Dereference","anchor":"verify-update-linked-art-event-dereference"},{"level":2,"heading":"Cursor Sync","anchor":"cursor-sync"},{"level":2,"heading":"IIIF Walk-Back Harvest","anchor":"iiif-walk-back-harvest"},{"level":2,"heading":"Object Feed","anchor":"object-feed"},{"level":2,"heading":"Register A Callback","anchor":"register-a-callback"},{"level":2,"heading":"Verify Locally After Partner Reads","anchor":"verify-locally-after-partner-reads"},{"level":2,"heading":"Partner Response Template","anchor":"partner-response-template"}],"html":"<h1 id=\"activitystreams-partner-quickstart\">ActivityStreams Partner Quickstart</h1>\n<p>Last updated: 2026-07-07</p>\n<p>This quickstart helps an external partner read the MetaMuseum ActivityStreams feed. It is onboarding documentation only. It does not count as external adoption evidence until a real partner-owned consumer ID, public feed read, durable HTTPS subscription, recent 2xx callback verification, and refreshed syndication evidence packet are recorded.</p>\n<h2 id=\"what-to-ask-for\">What To Ask For</h2>\n<p>Ask the partner for:</p>\n<ul><li>A stable partner-owned consumer ID, such as `museum-reader-prod`.</li><li>The system or organization that owns that consumer ID.</li><li>A public HTTPS callback URL if they can receive activity notifications.</li><li>A contact who can confirm observed activity types and callback results.</li><li>A real evidence reference, such as an issue, ticket, log export, or email thread.</li></ul>\n<p>Do not use demo, fixture, local, placeholder, sample, smoke, test, localhost, private-network, or MetaMuseum-owned IDs as strict evidence.</p>\n<h2 id=\"read-the-feed\">Read The Feed</h2>\n<p>Use the partner-owned ID in `x-linked-art-consumer-id`.</p>\n<pre><code>\ncurl &quot;https://www.metamuseum.org/api/activity?limit=100&quot; \\\n  -H &quot;accept: application/activity+json&quot; \\\n  -H &quot;x-linked-art-consumer-id: museum-reader-prod&quot;\n</code></pre>\n<p>For strict adoption evidence, read each required activity type with the same</p>\n<p>declared header as well. If a type-filtered URL returns zero items, report that</p>\n<p>explicitly so operators can distinguish missing feed content from missing</p>\n<p>partner coverage.</p>\n<pre><code>\ncurl &quot;https://www.metamuseum.org/api/activity?limit=100&amp;type=Create&quot; \\\n  -H &quot;accept: application/activity+json&quot; \\\n  -H &quot;x-linked-art-consumer-id: museum-reader-prod&quot;\n\ncurl &quot;https://www.metamuseum.org/api/activity?limit=100&amp;type=Update&quot; \\\n  -H &quot;accept: application/activity+json&quot; \\\n  -H &quot;x-linked-art-consumer-id: museum-reader-prod&quot;\n\ncurl &quot;https://www.metamuseum.org/api/activity?limit=100&amp;type=Delete&quot; \\\n  -H &quot;accept: application/activity+json&quot; \\\n  -H &quot;x-linked-art-consumer-id: museum-reader-prod&quot;\n</code></pre>\n<p>The response is an ActivityStreams `OrderedCollectionPage`. Each item includes:</p>\n<ul><li>`type`: `Create`, `Update`, or `Delete`</li><li>`object.id`: affected dereferenceable MetaMuseum Linked Art object URI</li><li>`object.sourceId`: original provider/stored object URI when it differs from `object.id`</li><li>`object.activityHref`: object-scoped feed URL</li><li>`object.equivalent[]`: reconciliation hooks such as provider URIs or Wikidata QIDs when available</li><li>`linkedArt`: embedded Linked Art JSON-LD projection; `linkedArt.id` is a dereferenceable `/api/events/{encodedSourceActivityId}` URL, embedded projections carry `_complete: false`, and `linkedArt.equivalent[]` preserves the source activity URI when available</li><li>`source.kind`: `audit`, `record-backfill`, `record-semantic`, or `deletion`</li><li>`endTime`: valid UTC sync timestamp; original placeholder times are retained under `extensions.sourceEndTime`</li><li>`nextCursor`: stable cursor for incremental sync</li></ul>\n<p>Default cursor-feed reads collapse record-backed diagnostic rows to the latest</p>\n<p>harvestable public row per object, matching the IIIF Change Discovery projection.</p>\n<p>Use `source=record-semantic` or another `source=` filter when you need the</p>\n<p>underlying semantic diagnostic rows instead of the deduped partner view.</p>\n<h2 id=\"verify-update-linked-art-event-dereference\">Verify Update Linked Art Event Dereference</h2>\n<p>When `type=Update` returns at least one item, fetch the first row&#39;s</p>\n<p>`orderedItems[0].linkedArt.id`. The response should return HTTP `200` with the</p>\n<p>Linked Art JSON-LD context, `entity.id` equal to the requested URL,</p>\n<p>`entity._complete: false`, and `entity.equivalent[]` preserving the source</p>\n<p>activity URI.</p>\n<pre><code>\ncurl &quot;https://www.metamuseum.org/api/activity?limit=100&amp;type=Update&quot; \\\n  -H &quot;accept: application/activity+json&quot; \\\n  -H &quot;x-linked-art-consumer-id: museum-reader-prod&quot;\n\ncurl &quot;&lt;orderedItems[0].linkedArt.id&gt;&quot; \\\n  -H &quot;accept: application/ld+json&quot; \\\n  -H &quot;x-linked-art-consumer-id: museum-reader-prod&quot;\n</code></pre>\n<p>If `type=Update` is zero, report zero and skip this dereference check. If</p>\n<p>`type=Delete` is zero, report zero explicitly as well; MetaMuseum does not emit</p>\n<p>Delete rows until a genuine upstream `404` or `410` tombstone exists.</p>\n<h2 id=\"cursor-sync\">Cursor Sync</h2>\n<p>Read the first page, store `nextCursor`, then request the next page with `cursor`.</p>\n<pre><code>\ncurl &quot;https://www.metamuseum.org/api/activity?limit=100&amp;cursor=&lt;nextCursor&gt;&quot; \\\n  -H &quot;accept: application/activity+json&quot; \\\n  -H &quot;x-linked-art-consumer-id: museum-reader-prod&quot;\n</code></pre>\n<p>Useful filters:</p>\n<ul><li>`type=Create`, `type=Update`, or `type=Delete`</li><li>`source=record-backfill`, `source=record-semantic`, `source=audit`, or `source=deletion`</li><li>`provider=met`, `provider=vanda`, `provider=museumsvictoria`, or another provider key</li><li>`object=&lt;encoded object URI&gt;`</li><li>`since=&lt;ISO timestamp&gt;`</li><li>`until=&lt;ISO timestamp&gt;`</li></ul>\n<p>When `limit` is absent, the feed returns up to `25` items. Explicit limits are clamped to `1..100`. The canonical `next` link uses a cursor only when the page boundary has a valid timestamp; otherwise it falls back to the offset `nextPage` link instead of emitting a bad cursor.</p>\n<h2 id=\"iiif-walk-back-harvest\">IIIF Walk-Back Harvest</h2>\n<p>For IIIF Change Discovery-compatible consumers, discover the fixed-page collection first:</p>\n<pre><code>\ncurl &quot;https://www.metamuseum.org/api/activity/collection?limit=100&quot; \\\n  -H &quot;accept: application/activity+json&quot; \\\n  -H &quot;x-linked-art-consumer-id: museum-reader-prod&quot;\n</code></pre>\n<p>Start from `last.id`, process that page in reverse item order if your mirror wants newest-first application, then follow each page&#39;s `prev.id` until you reach a timestamp older than your last crawl. Send `x-linked-art-consumer-id` on the collection request and every page request; MetaMuseum records those walk-back reads in the same readiness ledger as cursor-feed reads, and page reads record the concrete activity types observed on that page. The page documents themselves list activities oldest-to-newest so the most recent activity on a page appears last, matching the IIIF Change Discovery API 1.0 consumption model. The same filters used by cursor sync, including `provider=`, are preserved in `first`, `last`, `prev`, and `next` links.</p>\n<p>Discovery pages are thin by default: `linkedArt` is omitted, `endTime` is always a valid UTC timestamp, `object.equivalent[]` is present when stored records carry reconciliation hooks, and bare stored IDs are projected as dereferenceable `/api/records/{id}` object URLs with the original value preserved as `object.sourceId`. The cursor feed uses the same object/time projection while keeping `linkedArt` embedded. To include the richer Linked Art projection during traversal, add `embed=linked-art`:</p>\n<pre><code>\ncurl &quot;https://www.metamuseum.org/api/activity/collection?limit=100&amp;embed=linked-art&quot; \\\n  -H &quot;accept: application/activity+json&quot; \\\n  -H &quot;x-linked-art-consumer-id: museum-reader-prod&quot;\n</code></pre>\n<h2 id=\"object-feed\">Object Feed</h2>\n<p>For one object, use the encoded object URI:</p>\n<pre><code>\ncurl &quot;https://www.metamuseum.org/api/activity/object/https%3A%2F%2Fexample.org%2Fobject%2F123?limit=100&quot; \\\n  -H &quot;accept: application/activity+json&quot; \\\n  -H &quot;x-linked-art-consumer-id: museum-reader-prod&quot;\n</code></pre>\n<p>Record detail responses also advertise activity discovery with HTTP `Link`</p>\n<p>headers. A client that dereferences `/api/records/{id}` can find:</p>\n<ul><li>`rel=&quot;http://iiif.io/api/discovery/1#activity&quot;` for `/api/activity/collection`</li><li>`rel=&quot;alternate&quot;` for the cursor feed and object-scoped activity feed</li></ul>\n<p>The same activity metadata is listed under `/api/providers/capabilities` in</p>\n<p>`activitySyndication`.</p>\n<h2 id=\"register-a-callback\">Register A Callback</h2>\n<p>If the partner can receive events, register an HTTPS callback:</p>\n<pre><code>\ncurl -X POST &quot;https://www.metamuseum.org/api/activity/subscriptions&quot; \\\n  -H &quot;content-type: application/json&quot; \\\n  -H &quot;accept: application/activity+json&quot; \\\n  -H &quot;x-linked-art-consumer-id: museum-reader-prod&quot; \\\n  --data &#39;{&quot;callbackUrl&quot;:&quot;https://partner.example.org/metamuseum/activity&quot;,&quot;eventTypes&quot;:[&quot;Create&quot;,&quot;Update&quot;,&quot;Delete&quot;],&quot;signingRequired&quot;:true,&quot;signingSecretRef&quot;:&quot;museum-reader-prod-v1&quot;}&#39;\n</code></pre>\n<p>The callback URL must be public HTTPS and owned by the partner. When signing is enabled, the subscription stores only `signingSecretRef`; the actual HMAC secret is exchanged out of band. Callback deliveries are signed over `&lt;timestamp&gt;.&lt;raw JSON payload&gt;` with HMAC-SHA256 and include:</p>\n<ul><li>`x-metamuseum-signature`: `sha256=&lt;hex digest&gt;`</li><li>`x-metamuseum-signature-timestamp`: UTC signing timestamp</li></ul>\n<p>A subscription row alone is not enough for strict evidence; it also needs recent 2xx verification.</p>\n<h2 id=\"verify-locally-after-partner-reads\">Verify Locally After Partner Reads</h2>\n<p>After a real partner reads the feed:</p>\n<pre><code>\npnpm activity:adoption:matrix -- --base-url https://www.metamuseum.org --consumer-id museum-reader-prod\npnpm activity:subscription:verify -- --consumer-id museum-reader-prod --callback-url https://partner.example.org/metamuseum/activity --status-code 204 --verified-at &lt;ISO timestamp&gt; --evidence-ref &lt;real-proof-ref&gt;\npnpm activity:syndication:evidence\n</code></pre>\n<p>The evidence packet remains blocked until `consumerOnboarding.rows` shows the same partner consumer has feed-read telemetry, Create/Update/Delete type coverage, a durable subscription, callback verification, and strict packet refresh proof.</p>\n<h2 id=\"partner-response-template\">Partner Response Template</h2>\n<pre><code>\nConsumer ID:\nOrganization/system:\nFeed read timestamp:\nType-specific read timestamps:\nObserved activity types:\nUpdate linkedArt.id dereference:\nUpdate linkedArt.id notes:\nObject feed tested:\nCallback URL:\nCallback event types:\nCallback verification status code:\nCallback verification timestamp:\nEvidence ref:\nContact:\n</code></pre>","updatedAt":"2018-10-20T01:46:40.000Z","checksum":"6ea3aea16866eedf39659890c65fbf05ea7683ff2e74de9be1efa019b038bf88","checksumPrefix":"6ea3aea16866","anchorCount":9,"lineCount":196,"rawUrl":"/api/docs/content?path=linked-art%2Factivity-stream-partner-quickstart.md","htmlUrl":"/docs?doc=linked-art%2Factivity-stream-partner-quickstart.md","apiUrl":"/api/docs/content?path=linked-art%2Factivity-stream-partner-quickstart.md"}