Push, don't poll.
31 July 2026 · the launch journal
If your product needs to react to supply-chain events — a receiving discrepancy view, a recall workbench, a customer-facing "where is it" — the naive integration polls a query endpoint on a timer. Polling has a floor cost you pay forever: latency equal to your interval, load proportional to your tenant count, and a scheduler you now own. The EPCIS 2.0 query interface ships the alternative in the standard itself: subscriptions. Any query you can run, you can stand up as a standing query with a delivery destination, and events arrive as they land instead of when you ask.
This is not a vendor bolt-on beside the standard — it is the query interface's own mechanism, with the trigger vocabulary pinned in the official query schema this surface validates against (sha256 pins on the conformance page): second, minute, hour, dayOfMonth, month, dayOfWeek for scheduled evaluation, stream for trigger-on-capture, initialRecordTime to anchor the starting point, reportIfEmpty to distinguish "no news" from "no delivery."
The lifecycle, end to end
Create. A subscription is a named query plus a destination:
POST /queries/receiving-exceptions/subscriptions
content-type: application/json
{ "dest": "https://isv.example/hooks/epcis",
"signatureToken": "…",
"stream": true,
"reportIfEmpty": false,
"initialRecordTime": "2026-07-31T00:00:00.000Z" }
stream: true is trigger semantics: the query is evaluated against events as they are captured, and matches are delivered promptly. The schedule fields are the alternative — "minute": "0" delivers on the hour, cron-shaped, for consumers that want batches. One subscription carries one semantics; both are declared, never inferred.
Deliver. A delivery is an EPCISQueryDocument — the same shape a pull query returns, so your handler parses one format regardless of how the events arrived — plus a signature header computed over the payload with the subscription's signatureToken:
POST /hooks/epcis
GS1-Signature: sha-256=…
content-type: application/json
{ "type": "EPCISQueryDocument",
"epcisBody": { "queryResults": {
"queryName": "receiving-exceptions",
"subscriptionID": "sub-4f21…",
"resultsBody": { "eventList": [ … ] } } } }
Verify the signature before you trust the body. The destination is your endpoint on your domain; the token was set by you at create time; a delivery that fails verification is somebody else's problem wearing your URL.
Consume. The delivery contract, stated as the table your on-call engineer needs:
| Property | Contract |
|---|---|
| Delivery semantics | at-least-once — a delivery may repeat; it is never silently dropped |
| Retry | failed deliveries retry on a backoff ladder; the subscription survives your outage |
| Ordering | within one delivery, events are ordered; across deliveries, arrival order is not a promise — order by eventTime/recordTime yourself |
| Dedupe | by the CBV 2.0 §8.9 event hash — see below |
| Empty windows | delivered only if reportIfEmpty: true |
The hash closes the at-least-once gap
At-least-once delivery means your handler sees duplicates: a retry after a timeout your side actually processed, an overlap after an outage. Systems without content-derived identity handle this with delivery IDs and a dedup table keyed on the transport — fragile, because the transport is exactly what failed.
Here every event carries computed identity: the §8.9 hash of its own content, e.g. ni:///sha-256;28339e05…?ver=CBV2.0. Your consumer's dedupe is one set-membership check on a value that is derivable from the event itself — no coordination with the deliverer, no shared counter, and the same identity your pull queries, your partner's store, and the capture door's idempotency already agree on. Processing a delivery twice converges to processing it once. That is the whole exactly-once story, and it lives in the data, not the pipe.
Replay is a query, because the subscription is a view
The deepest property: a subscription holds no state your record does not already hold. It is a standing view over the same append-only store the pull interface reads. So catch-up after a consumer outage is not a support ticket about redelivery — it is a pull query with GE_recordTime set to the last event you processed, against the same query definition, returning the same document shape your webhook handler already parses. Run the backfill, resume the stream, dedupe by hash across the seam; the overlap is harmless by construction.
This is also the honest answer to "what if your webhook infrastructure has a bad day": the durable record is the source of truth, delivery is a convenience over it, and nothing about your correctness depends on the pipe being perfect. Compare that with event buses where the topic is the record and a retention window is quietly part of your data model.
Where this lands in an ISV product
The embed pattern: your product subscribes per tenant with the tenant's scoped key, your handler verifies, dedupes by hash, and projects events into your own UI's shape. Your customers see your product reacting in seconds; the standards machinery — query validation against the pinned schema, scoped reads, signed delivery, replay — is this layer's job, not your roadmap's. The same events join to the document layer (purchase orders, despatch advices, receiving advices) at /business-transactions/, and the executive views your buyers ask for are computed over the identical record — visibility.cloud is that reading.
Prove the query half on your own machine first: the local toolkit ships the full pull surface — capture into a session store, then query and trace over the MCP door or CLI, no account (run it yourself). Standing delivery rides the hosted surface with scoped keys: say what you will read, and name the destination when you do.
We answer in writing. We take at most five conversations a month, only when you ask for one, and only after you already have the written read.