By ipvolt · Checked 16 September 2026
Persist each validated Amazon listing notification and the work it creates in one transaction before acknowledging delivery. Use the notification to schedule a listing read. If another accepted notification arrives during that read, keep the work pending and read again. This prevents a specific local race: an older in-flight result clearing a newer refresh request.
This guide is for developers with an authorized SP-API application and an existing EventBridge delivery pipeline. Integration requires your own validated event adapter and authenticated listing-read adapter. The downloadable SQLite lab runs offline with synthetic events, one reconciliation worker and Python's standard library; it implements the persistence and completion checks between those adapters.
Method: ipvolt ran the demo and 12 tests on 16 September 2026 using CPython 3.14.7 and SQLite 3.53.4. Python 3.11+ is the source target; 3.11 was not exercised in that recorded run. No Amazon account, API request or real queue acknowledgment was tested.
Run the local lab
Download and extract the complete lab ZIP. Open a terminal in the directory containing consumer.py, then run:
python3 -B demo.py
python3 -B -m unittest -v test_consumer.pyThere are no packages to install. Both commands use temporary databases. The demo prints deterministic JSON; compare it with the recorded output.
The README documents the complete contract. Inspect consumer.py, demo.py, the test suite and synthetic fixture individually if you prefer.
Normalize a trusted event before storing it
Listing status and issues notifications use Amazon's EventBridge workflow. An SQS queue can be a downstream rule target; that is different from subscribing through the direct SP-API SQS workflow. Follow Amazon's EventBridge setup for destinations, subscriptions and delivery permissions.
Pin the raw type and payload version in your adapter. The lowercase names below belong to this lab:
| Documented Amazon type | Payload version | Normalized lab type |
|---|---|---|
LISTINGS_ITEM_STATUS_CHANGE | 1.0 | listing_status_changed |
LISTINGS_ITEM_ISSUES_CHANGE | 2023-12-13 | listing_issues_changed |
Amazon directs users of issues version 1.0 to migrate to 2023-12-13. Status events concern creation, deletion and buyability; issue events contain summaries that can prompt a fuller read. They do not describe every possible listing problem. Amazon notification types.
Both raw payloads allow MarketplaceId to be absent. Resolve it only through trusted, authorized configuration; otherwise hold the event for investigation before creating a normalized record. Never silently supply your default marketplace. The lab requires explicit seller, SKU and marketplace. See the pinned status schema and issues schema.
There is also a naming inconsistency: that status schema's enum says LISTINGS_ITEM_STATUS_CHANGED, while its example and the documentation say LISTINGS_ITEM_STATUS_CHANGE. Keep any validation exception narrow, versioned and covered by your adapter tests. The normalized lab neither parses that envelope nor certifies its schema conformance.
This is the exact first synthetic record in the fixture:
{
"schema_version": 1,
"source": "fixture:eventbridge:bus-A",
"subscription": "fixture-subscription-A",
"notification_id": "notice-001",
"seller_id": "seller-A",
"sku": "SKU-RED",
"marketplace_id": "market-A",
"notification_type": "listing_status_changed",
"event_time": "2026-09-16T10:00:00Z",
"payload": {"hint": "synthetic status change"}
}Store.ingest() accepts this object as JSON text. The adapter must validate source, application/subscription context, seller authorization and scope first. Preserve the inner notification identity separately from EventBridge's outer event ID and any queue receipt handle. The EventBridge envelope and SQS deletion contract describe those distinct transport values.
The lab deduplicates (source, subscription, notification_id). This composite is an application policy. Keep delivery-attempt counters, receipt handles and receive times out of the normalized object: they change between deliveries. The README lists its strict field, size and JSON validation bounds.
Commit acceptance and pending work together
EventBridge can deliver duplicates and provides no ordering guarantee. A downstream SQS standard queue also permits duplicates and out-of-order delivery. AWS delivery comparison, SQS standard queues.
The executable core handles that uncertainty in four steps:
- Start a SQLite transaction. For a new identity, insert the inbox record and increment the work generation for
(seller_id, sku, marketplace_id)together. - For an existing identity, compare canonical normalized content. Identical content returns
duplicatewithout incrementing work. Different content raisesIdentityConflictand preserves the original state. - Return
may_ack=Trueonly after commit. This is a logical decision for your transport adapter; the download makes zero queue acknowledgment calls. Validation, conflict and storage errors do not produce that decision. - Scan
dirty_keys()after restart and between runs. A key is dirty whengeneration > applied_generation. Capture its generation, perform the read outside the transaction, then save the result only if the key and generation still match.
Every newly accepted identity advances its key, including an event with an older event_time. The payload is retained for identity comparison and never applied as current listing state. The inspected status model defines EventTime as a timestamp, not a shared listing revision. This design does not discard work merely because its timestamp is older.
Observe the restart and in-flight read race
This compact trace is drawn from the recorded demo for seller-A / SKU-RED / market-A:
| Case | Generation | Applied generation | Dirty | Observed result |
|---|---|---|---|---|
| Process exits after commit | 1 | 0 | true | One inbox row survives |
| Redelivery after restart | 1 | 0 | true | duplicate, may_ack: true |
| Initial synthetic read | 1 | 1 | false | read-1 saved |
| Older event arrives | 2 | 1 | true | New refresh required |
| Another event arrives during generation-2 read | 3 | 1 | true | completion_applied: false |
| Following read fails | 3 | 1 | true | Previous snapshot retained |
| Later successful refresh | 3 | 3 | false | read-3 saved |
The generation-2 result cannot clear generation 3. Its rejected read-2 document never replaces read-1; the subsequent failure also leaves work pending. The demo finishes with five inbox rows, three independent clean keys and zero external acknowledgments.
All 12 recorded tests pass. They include a hard child-process exit after commit, injected SQLite failures that roll back inbox and work together, conflicting identities, wrong-key results and the race above. This establishes local process-restart behavior, not power-loss or filesystem-corruption durability.
“Clean” means a read was accepted against the latest persisted local generation. It cannot prove that Amazon's response includes the triggering change, that another notification is not delayed, or that delivery was complete. The counter is not an Amazon revision or a distributed exactly-once guarantee.
Validate the read before clearing work
Your read adapter must choose getListingsItem datasets explicitly and validate HTTP status, response content and the intended seller/SKU/marketplace meaning before returning ReadResult. Amazon's listing retrieval tutorial explains which datasets answer which questions. It now supports multiple marketplaces in the same region for sellers; this lab deliberately keeps one marketplace per work key.
The core does not validate a real Amazon response. It checks the adapter-declared key, JSON bounds and local generation. Even {} passes its object-shape check. If required data is missing, the response belongs elsewhere, or the request fails, your adapter must raise instead of returning a success-shaped placeholder. refresh_once() then leaves the work dirty and the previous snapshot intact.
For the separate decision about accepted submissions, current offers, inventory and unknown observations, use Amazon listing updates: accepted is not live. A successful storage operation cannot make an incomplete observation meaningful.
Add recovery around the core
Use one reconciliation worker for this example. Its tickets are generation checks, not worker leases. The database retains inbox identities indefinitely; deleting them changes the deduplication horizon. Scheduling, backoff, rate limits, retention and concurrent worker ownership require additional design.
A production pipeline also needs durable handling for malformed or conflicting events, delivery-failure recovery and periodic reconciliation of authorized listing scope. Amazon recommends a backup retrieval mechanism; EventBridge stops retrying after its configured policy is exhausted and supports a dead-letter queue. A sweep and DLQ recovery are outside this lab. They cover failures an inbox cannot see because the event never reached it.
ipvolt is in development. Join the early-access list for one email when access opens. Nothing else.
Sources & further reading
Technical references used for this guide. Check the documentation for your installed version and your provider’s supported configuration.
- Set up notifications using the Amazon EventBridge workflow
- Notification Type Values
- Listings Item Status Change Notification schema
- Listings Item Issues Change Notification schema2023-12-13
- AWS service event metadata
- DeleteMessage
- Amazon SQS, Amazon SNS, or Amazon EventBridge?
- Amazon SQS standard queues
- Retrieve details about a listing for single or multiple Amazon stores
- Notifications API
- How EventBridge retries delivering events