A composite support thread, not a measured incident, shows the failure in one place. A generated reference page listed Idempotency-Key on POST /charges. The row included a type, a max length, and a UUID example. A client retried a timed-out POST with the same key and a different amount, and a second charge landed.
The page never stated scope, retention, or the mismatch status. Every cell the schema could fill was filled. The cells that were absent were the ones support had to invent on a call.
That pattern is common in generated reference docs. Generators copy columns well. They do not own product promises.
Two layers in one header
Idempotency documentation is two artifacts that happen to share a heading. One is a field table. The other is a behavior contract.
The field table answers parser questions: name, location, required flag, pattern, example. The behavior contract answers client questions: what the key is scoped to, how long a replay is honored, and what a mismatched body returns.
Collapse those layers into one fluent paragraph and an optional string starts to read like an SLA. Keep them in separate files until a human attests the second file.
What generation may fill
A generator, including a model, may fill a row only when the repo already contains the fact. The test is mechanical: can you point at a file that already says it?
Allowed inputs:
- The OpenAPI parameter object:
name,in,required,schema, andexampleif present. - A passing contract test whose name is cited beside the status it asserts.
- A fixture path recorded next to the example value.
- A glossary sentence copied as a quote, not extended with new scope language.
Every drafted sentence should carry a source marker, for example <!-- src: openapi#/paths/~1charges/post -->. If the marker cannot be produced from the snapshot, the sentence stays out of the draft file. Silence is a valid output.
What a human must own
An API owner, with support policy in the room, must write and attest the following before the page is public. The list is short on purpose. Each item is a commitment, not a description.
- Scope: account, route, or a stated composite such as account plus route.
- Replay window: a duration with a unit, or the explicit phrase not published.
- Mismatch rule: same key, different body, which status, and whether the first response is replayed.
- Stored material: raw key, hash only, response snapshot, or request body.
- Failover claim, if any: what happens when the store that remembers the key is unreachable.
These lines are promises. A blank that says not published is safer than a smooth sentence nobody signed.
Decision table
Use the table as a publish gate, not as inspiration for extra columns. Fill means a model may propose wording from a cited source. Attest means a named human accepts the line.
| Element | Typical source | Mode | Publish rule |
|---|---|---|---|
| Header name and location | OpenAPI | Fill | Block if the parameter is absent |
| Required flag | OpenAPI required
|
Fill | Must match the gateway config you extracted |
| Pattern and max length | OpenAPI schema | Fill | Quote the schema; do not tighten it in prose |
| Example value | Schema example or fixture path | Fill | No invented story around the example |
| Scope | Product note | Attest | Empty or not published until signed |
| Replay window | Product note | Attest | No duration without an attestation id |
| Mismatch status | Contract test | Fill only with test name | Otherwise Attest |
| Stored fields | Security review | Attest | Never inferred from sample logs |
| Client retry list | Error catalog version | Quote | Human confirms the quoted list |
Adding a column because a model usually includes it is a spec change. Spec changes do not ride along in a docs pull request.
Six-step workflow
Run the split on one operation before you scale it. The point is a blocked publish, not a longer page.
- Snapshot. Write the parameter block and matching test names to
build/idempotency/sources.json. Do not hand-edit that file. - Separate. Create
fields.mdfor sourced rows andpromises.mdfor blanks. Startpromises.mdwith not published, not with a guess. - Fill. Point a model at
sources.jsonandfields.mdonly. Do not putpromises.mdin the same context. - Scan. Run the checker below on the combined source before render.
- Attest. The API owner replaces each not published they are willing to stand behind, and adds an attestation marker.
- Release. Publish only when the checker exits 0 and each remaining not published is an intentional public statement, not an oversight.
Steps 1, 4, and 6 belong in CI. Step 5 does not. If the same account can both fill and waive attestation, the gate is decorative.
Commands for the snapshot
The commands below are illustrative. They are not a log from a live repository, and the paths assume a conventional layout.
mkdir -p build/idempotency
jq '.paths["/charges"].post.parameters[]
| select(.name=="Idempotency-Key")' openapi.json \
> build/idempotency/parameter.json
rg -n "Idempotency-Key" tests/contract -g '*.json' \
> build/idempotency/test-hits.txt
A minimal sources.json shape, also illustrative:
{
"operation": "POST /charges",
"parameter_file": "build/idempotency/parameter.json",
"tests": ["tests/contract/charges_retry.json"],
"extracted_at": "set-by-ci"
}
The timestamp is a CI value. Do not ask a model to invent it.
Checker script
The script is a proposal. It has not been run on a production tree for this article. It flags duration and scope phrasing that lacks an attestation marker.
#!/usr/bin/env python3
"""Flag idempotency promise lines that lack an attestation marker.
Proposal only. Not a measured scan.
"""
import re
import sys
from pathlib import Path
PROMISE = re.compile(
r"\b(replay window|retained for|stored for|scoped to|per account|"
r"per merchant|request body is kept|\d+\s*"
r"(seconds?|minutes?|hours?|days?))\b",
re.I,
)
MARKER = "promise-attested:"
def main() -> int:
path = Path(sys.argv[1])
hits = []
for i, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
if PROMISE.search(line) and MARKER not in line:
hits.append(f"{path}:{i}: unsigned promise language")
if hits:
print("\n".join(hits))
return 1
print("no unsigned promise lines")
return 0
if __name__ == "__main__":
raise SystemExit(main())
Run it on the source file CI will publish from, not on the rendered HTML. A green process exit is an input to review, not a substitute for it.
python3 check_idempotency_promises.py docs/charges.md
An attested line in source:
Replay window: 24 hours. <!-- promise-attested: api-owner, note idem-04 -->
The marker is intentionally plain. Renderers may strip HTML comments. The file CI reads must still contain the marker, or the gate checks the wrong artifact.
What a green run does not prove
A pass means the listed phrases were either absent or marked. It does not mean the duration is true. It does not catch a paraphrase such as duplicates collapse through the next settlement cutover. Pair the script with the table, and still read every page that mentions replay or retention.
False confidence is the main risk. A short regex feels like policy. It is only a tripwire.
A free pass for the field rows
The expensive part of this split is not the attestation meeting. It is producing field rows for many operations without letting blanks get completed in the same breath.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Operator-supplied availability, not a benchmark: MonkeyCode's free model access can run that field-row pass on schema excerpts, and a free server option can run the checker as a small job next to CI. This article states no token quota, model name, hardware size, time limit, or permanence. Confirm current terms before you depend on either option.
If those options are unavailable, the same files work with any model you can constrain to sources.json. The useful constraint is context, not brand: keep promises.md out of the prompt. Send the parameter JSON, the test-hit list, and an instruction to emit not published for scope and window.
Reject any completion that adds minutes, accounts, or storage claims. That rejection is the workflow. The model is not the author of the promise.
If you try the split, start with one operation and the checker in CI. Confirm the free model access and free server option still match your constraints before you make either a required check.
Limitations
The approach has a narrow job. It stops fluent invention of replay policy. It does not certify the API.
- The regex misses paraphrase. Plan a human read for any page that discusses retries.
- OpenAPI can disagree with the gateway. A filled required cell is only as current as the snapshot.
- A test that expects HTTP 200 on retry does not define a window. Do not let the test name smuggle a duration into prose.
- Free model access and a free server option can be withdrawn or limited. They are not an uptime commitment for your docs pipeline.
- Storage lines can be a privacy issue. Security review owns them even when the table says Attest.
- Client samples do not belong in the promise file. A syntax edit must not be able to rewrite a retention sentence.
Who should skip this
Skip it when you have neither an OpenAPI document nor contract tests. There is no honest source to fill from, and a model will treat the gap as a writing prompt.
Skip it when the team wants a suggested window because nobody has decided. Decide in the product note. Then let docs quote the note.
Skip it on public payment or booking APIs where the replay sentence is a customer commitment, unless counsel already owns that sentence. A docs job should not become the author of record for a charge guarantee.
Skip it when one person can generate the page and override the checker to meet a release time. Unsigned rows have to fail the build, or the split is a style guide nobody runs.
Keep the promise unsigned until a person signs
Field rows are a rendering task. Replay windows are a product promise. Let generation handle the first. Put a name, a note id, and a date on the second.
Publish not published when the decision is not ready. That sentence is accurate. A guessed retention period is not.
Top comments (0)