Detect Sume OpenAPI drift in CI: hash the operations you call

Fetch api.sume.com/reference/json, hash only the operations you use, and fail CI when one changes. A 23-line script, plus the User-Agent a stdlib fetch needs.

4 min readSume
All posts

Download https://api.sume.com/reference/json, hash the JSON of only the operations your code calls, and compare the hashes with a committed file; a mismatch fails the build and tells you to read the diff. This catches a changed request or response schema before your first failed production call.

The API overview calls this document the schema source of truth and says the docs snapshot is refreshed from it. When I fetched it on 2026-10-10 it was OpenAPI 3.0.3 with 171 paths and about 2 MB, so you want to watch slices, not the whole file.

Why hash operations and not the file

A hash of the whole document changes whenever anything anywhere changes, including routes you never call. Hashing one operation object (parameters, request body, responses) changes only when your contract does. The table lists the unit and what a change means.

Drift units and what a hash change tells you (OpenAPI document, read 2026-10-10)
Unit hashedChanges whenLikely action
Whole documentAnything in 171 paths changesToo noisy to gate on.
One operation (method + path)Its params, body or responses changeRead the diff; update types and tests.
One component schemaA shared type changesFind every operation that references it.

The script

It fetches the document with a custom User-Agent, because the default Python-urllib agent got a 403 from this host in my test; see the linked post. It writes sume-watch.json on the first run, and on later runs it prints the changed operations and exits 1 if any differ.

import hashlib, json, sys, urllib.request

WATCH = [("get", "/v1/balance"), ("get", "/v1/jobs/{id}/status"), ("get", "/v1/jobs")]
req = urllib.request.Request("https://api.sume.com/reference/json",
                             headers={"User-Agent": "spec-drift-check/1.0"})
spec = json.load(urllib.request.urlopen(req, timeout=60))

def digest(method, path):
    op = spec["paths"][path][method]
    return hashlib.sha256(json.dumps(op, sort_keys=True).encode()).hexdigest()[:12]

now = {f"{m} {p}": digest(m, p) for m, p in WATCH}
try:
    old = json.load(open("sume-watch.json"))
except FileNotFoundError:
    old = None
json.dump(now, open("sume-watch.json", "w"), indent=1)
if old is None:
    print("baseline written")
else:
    changed = [k for k in now if old.get(k) != now[k]]
    print("changed:", changed or "none")
    sys.exit(1 if changed else 0)

Using it well

Commit sume-watch.json, and run the script on a schedule rather than on every pull request, since the spec can change without your code changing. When it fails, diff the operation, update the code and the file in one commit, and note the date.

Do not treat a green run as proof that behavior is unchanged. The document describes shapes; behavior such as admission, retries and queueing lives in the prose docs, which this script does not see. Pair it with the pacing and retry tests you already have.

  • Pin the three or four operations you actually call.
  • Keep the snapshot in the repo, not in a cache.
  • The document is public; you need no key to fetch it.

What to do when the hash changes

A changed hash is a prompt to read, not a failure to fix at once. Print the old and new operation JSON, look for removed fields, new required parameters or new enum values, and run your contract tests against the new shape. Most changes in a growing API add optional fields, which your code can ignore.

Commit the watch file with the code that uses the operations, so a pull request that bumps a hash shows the reviewer which calls were reviewed. Fetch the spec with a custom User-Agent, as the urllib 403 post explains, and fetch it on a schedule rather than on every build, since the file is about 2 MB.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume