Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Subscriptions

A subscription says: tell me when an artifact matching this appears. It belongs to a deployment, which is what makes it a tool-to-tool mechanism rather than a notification feature — the deployment that wants to know is the one that will act on it.

GET    /api/v1/instances/{id}/subscriptions
POST   /api/v1/instances/{id}/subscriptions
GET    /api/v1/subscriptions/{sid}
PATCH  /api/v1/subscriptions/{sid}
DELETE /api/v1/subscriptions/{sid}
GET    /api/v1/subscriptions/{sid}/deliveries
POST   /api/v1/subscriptions/{sid}/deliveries/ack

Managing a subscription needs admin, curator, or the credential of the deployment that owns it. A mismatched subscription id returns 403 rather than 404, so the endpoint cannot be used to enumerate what exists.

Creating one

curl -X POST -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
     -d '{
       "label": "shapes graphs to validate against",
       "filter": {
         "conforms_to": ["https://registry.example.org/type/shacl-shapes-graph"],
         "availability": ["public", "restricted"],
         "roles": ["produced"],
         "exclude_own": true
       },
       "webhook_url": "https://validator.example.org/hooks/tar"
     }' \
     https://registry.example.org/api/v1/instances/$INSTANCE_ID/subscriptions

Omit webhook_url and the subscription is pull-only. That is the whole difference between the two modes; there is one queue underneath.

The filter

Every field is a list, and they combine OR within a field, AND across fields. An empty filter matches everything.

FieldMatches
conforms_toArtifact type IRIs.
softwareAny deployment of that software.
instanceOne named deployment.
keywordsdcat:keyword, case-insensitively.
licenseSPDX IRIs, exactly. An artifact with no licence never matches a non-empty licence filter.
availabilitypublic, restricted, embargoed, metadata-only.
qSubstring of title or description, case-insensitively.
rolesproduced, consumed. Empty means produced only.
exclude_ownDefault true — do not notify a deployment about its own output.

Registry-minted software and deployment ids may be sent bare and are expanded server-side. Type and licence IRIs must be full IRIs, because the registry has no basis for guessing what a bare one meant.

exclude_own defaults to true because the overwhelmingly common subscription is "tell me what somebody else made", and a tool woken by its own output is a loop.

Filter on the type, not the keyword

A subscription written against a keyword is a subscription written against a spelling. This is the case where the vocabulary rules earn their keep: a subscription that never fires is indistinguishable from a quiet week, so nobody notices it is broken.

Webhook delivery

The registry POSTs to webhook_url:

POST https://validator.example.org/hooks/tar
x-tar-delivery: 01a05…
x-tar-subscription: 01a05…
x-tar-timestamp: 1756567331
x-tar-attempt: 1
x-tar-signature: sha256=<hex>

{
  "type": "artifact.advertised",
  "subscription": "…", "registry": "…",
  "role": "produced",
  "run": "…", "instance": "…", "software": "…",
  "artifact_iri": "…",
  "artifact": { … }
}

artifact is exactly what an anonymous GET /api/v1/artifacts/{id} returns, so a receiver does not have to call back for the ordinary case.

Verifying the signature

x-tar-signature is sha256= followed by the hex HMAC-SHA256 of "{timestamp}.{body}" under the subscription's secret. Supply webhook_secret when you create the subscription, or let the registry generate one — it is returned once.

Include the timestamp in what you verify, and reject old ones. Signing the body alone would let anyone who saw one delivery replay it forever.

What the registry will not deliver to

HTTPS only unless TAR_SUBSCRIPTION_ALLOW_HTTP is set, and never to a private, loopback or link-local address unless TAR_SUBSCRIPTION_ALLOW_PRIVATE_TARGETS is set. Redirects are not followed.

A webhook URL is chosen by whoever registers the subscription and can point anywhere, so refusing private targets is what stops the registry being used to reach inside a network on somebody's behalf. The address is checked at registration and re-resolved at send time, and the delivery then connects to exactly the address that check approved — the name is not looked up again, so a record that changes in between cannot redirect the connection. The certificate is still verified against the hostname.

Note the contrast with health probing, which allows private addresses by default. The two look alike and are not: a deployment's endpoint is an address in your own estate, and for an internal registry it is normally private.

Retries

Default
TAR_SUBSCRIPTION_MAX_ATTEMPTS8Attempts before one delivery is marked dead.
TAR_SUBSCRIPTION_SUSPEND_AFTER12Consecutive failures before the subscription's webhook is suspended.
TAR_SUBSCRIPTION_BACKOFF_BASE30s
TAR_SUBSCRIPTION_BACKOFF_MAX6h
TAR_SUBSCRIPTION_TIMEOUT5sPer attempt, capped at 30s.
TAR_SUBSCRIPTION_TICK5sWorker poll interval.
TAR_SUBSCRIPTION_BATCH20Deliveries attempted per tick.
TAR_SUBSCRIPTION_WEBHOOKStrueThe delivery worker at all.

Backoff is base × 2^(attempts−1), capped at max.

Suspension stops webhook attempts; the pull path keeps working, so a subscription whose receiver was down for a day is not a subscription that lost its data. PATCH {"resume": true} un-suspends it and re-arms failed and dead deliveries for another attempt.

Pull delivery

For a tool that cannot accept an inbound connection — behind a firewall, on a laptop, running only during a job.

curl -H "Authorization: Bearer $TOKEN" \
     'https://registry.example.org/api/v1/subscriptions/01a05…/deliveries?limit=50'

Returns the queued deliveries plus next_cursor and remaining. The cursor is a monotonic sequence number, not an IRI. limit defaults to 25 and is clamped 1–200.

Acknowledge either by passing ack=true on the read — which acknowledges everything in that response — or afterwards:

curl -X POST -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
     -d '{"cursor": 4711}' \
     https://registry.example.org/api/v1/subscriptions/01a05…/deliveries/ack

The acknowledged cursor only ever advances. Acknowledging an older value is a no-op rather than a rewind, so a slow consumer racing itself cannot replay what it has already handled.

Omit the cursor on a read and you resume from wherever you last acknowledged.

ack=true is the convenient form and the lossy one: if your process dies between receiving the response and acting on it, that work is gone. Acknowledge separately if the work matters.