A saved OpenRTB response can be valid JSON and still bid on an impression that was never offered. Once the originating request is discarded, the evidence needed to establish that relationship is gone.

A replay fixture should preserve the request, response and interpretation context together. The pair lets you test relationships that neither document can establish independently.

This walkthrough checks request identity, impression matching and response currency. The examples are deliberately incomplete fragments. They are not full OpenRTB validators or examples of auction-ready traffic.

Keep the smallest useful pair

Start with a request containing two impression IDs and an explicit allowed currency. Keep a response with one bid pointing to one of those impressions.

Do not reduce the fixture to a response and a comment describing the request. Comments drift, while an executable relationship test can fail when an adapter changes a field.

Preserve the OpenRTB specification snapshot, exchange profile, request headers relevant to interpretation and validator version beside the pair. A field's JSON type alone does not identify the contract under which the exchange processed it.

For a historical failure, retain both a controlled original capture and a sanitized regression fixture. The latter should reproduce the relevant structure without carrying unnecessary production identifiers or personal data into the repository.

Check the relationships explicitly

The following complete Node.js program assumes basic structural validation has already established the expected arrays and strings:

import assert from 'node:assert/strict'; function pairingIssues(request, response) { const issues = []; if (response.id !== request.id) issues.push('request ID mismatch'); const ids = new Set(request.imp.map(imp => imp.id)); if (ids.size !== request.imp.length) issues.push('duplicate request impression ID'); for (const seat of response.seatbid ?? []) { for (const bid of seat.bid ?? []) { if (!ids.has(bid.impid)) issues.push(`unknown impid: ${bid.impid}`); } } const currency = response.cur ?? 'USD'; if (request.cur && !request.cur.includes(currency)) { issues.push('response currency not offered'); } return issues; } const request = { id: 'auction-17', cur: ['EUR'], imp: [{ id: 'slot-a' }, { id: 'slot-b' }] }; const response = { id: 'auction-17', cur: 'EUR', seatbid: [{ bid: [{ id: 'bid-1', impid: 'slot-b' }] }] }; assert.deepEqual(pairingIssues(request, response), []); const wrongImp = structuredClone(response); wrongImp.seatbid[0].bid[0].impid = 'slot-c'; assert.deepEqual(pairingIssues(request, wrongImp), ['unknown impid: slot-c']); assert.deepEqual(pairingIssues(request, { ...response, id: 'auction-18' }), ['request ID mismatch']); assert.deepEqual(pairingIssues(request, { ...response, cur: 'USD' }), ['response currency not offered']); const omittedCurrency = structuredClone(response); delete omittedCurrency.cur; assert.deepEqual(pairingIssues(request, omittedCurrency), ['response currency not offered']); 

The last case matters because the response currency defaults to USD in the referenced OpenRTB snapshot. Omitting it does not make an EUR-only request compatible with an otherwise unchanged bid.

Notice that the example does not pair by array position. The response's impid identifies an offered impression. Reordering the request's impression array should not change that relationship.

Create a failing fixture for one rule at a time

Keep the clean pair as the baseline. Clone it and alter one relationship, such as the response request ID or one bid's impression reference.

This makes the expected finding unambiguous. A fixture with several unrelated defects can continue failing after the defect you intended to protect has been accidentally removed.

Add a request with duplicate impression IDs as a separate structural or relationship case. If two offered impressions share an identifier, a lookup can appear to succeed while the association remains ambiguous.

Also reorder the request and response arrays in a valid fixture. That catches implementations that happen to pass because the first response bid points to the first request impression in the original test data.

Preserve relationships while redacting captures

Sanitizing every identifier independently can manufacture a new failure. Use a consistent replacement map for the request ID and every cross-reference you intend to preserve.

Do the same for deal IDs and other linked identifiers as you extend the fixture. If the original defect concerns an unmatched deal, preserve that mismatch deliberately rather than correcting it during redaction.

Replace production URLs and identity data with synthetic values that retain the relevant shape. Keep the original capture under the appropriate access controls when it is needed for the investigation.

Re-run the same expected findings on the sanitized fixture. Redaction is a transformation that can change behavior, so its output needs its own check.

Extend the pair according to your integration

After the basic identity checks, add the relationships your exchange profile requires: selected deal, permitted media characteristics, creative dimensions, duration or other request-dependent conditions.

Keep pricing units and currencies together when checking floors. A response can use an allowed currency while its amount still needs conversion before comparison with the applicable floor.

Separate request-independent findings from pair-dependent findings. A missing required response field is different from a bid whose value conflicts with this particular request. Distinct categories make partner tickets and regression results easier to interpret.

Do not treat a clean result from these few checks as evidence that a bid should win. Auction acceptance also depends on rules outside the helper and on the actual exchange implementation.

Make the replay result reproducible

Store the fixture hashes, validator release, selected profile and expected findings with the run. A command that produces no output is less useful than a result identifying exactly which pair and rule set were tested.

When a validator upgrade changes the result, compare the findings against the same immutable pair before changing the fixture. Otherwise a simultaneous payload edit and rule update can hide the cause.

Keep timing evidence alongside the pair when the incident involved a deadline. An offline response can satisfy every relationship in this helper and still have arrived too late for the exchange to use it. State that limit in the replay result.

I maintain RTBlint. Its request and response tester helps inspect the pair. Keep that pair in your own regression corpus so a later failure can be reproduced without reconstructing the original auction from incomplete logs.

Sources

Technical sources checked October 25, 2026. Drafted with AI; examples are synthetic unless explicitly stated otherwise.