hamsa software
← All articles
Integrations7 min read

The API response still parses

Five mixed-version records expose the difference between valid JSON, representable unknown states and safe status-dependent actions.

A triangle receives one incoming line and releases five diverging lines, suggesting several distinct outcomes from one decoder.

The row disappeared after a successful response

The report renders without an error, but E3 has vanished. Its new status, held, is valid JSON; it simply does not belong to the queued-and-sent filter the application already understands. The parser has done its job while the overview has quietly lost a record. That is a useful compatibility question to ask before celebrating a successful API upgrade: can the client keep what arrived visible, even before it knows what the new value permits it to do?

The example is an invented message-status API. Version one uses queued and sent; version two adds held. The names describe a synthetic contract, not a historical change made by GitHub or another provider. Its purpose is to separate three questions: can the payload be parsed, can the programme represent what arrived, and does it know which business action is appropriate? Those questions can have different answers for the same record.

GitHub's current versioning policy provides a real reason to plan for this distinction: it lists additions including new enum values among additive changes that can appear across supported versions.[1] The policy distinguishes a supported API version from a frozen vocabulary of response values. That distinction affects the client’s parser, stored records and handling of values its author has not yet encountered. A version header remains useful, but pinning a version should not be treated as a promise that every response will forever contain exactly the values the client author first encountered.

Keep unfamiliar values representable

The fixture contains E1 queued and E2 sent from the old version, E3 held from the new version, E4 queued with an added nullable reason, and E5 without status. The old known-status filter displays E1, E2 and E4 only. That result is incomplete for an overview of all work: it excludes both the new state and the contract failure. Keeping those cases visible is necessary before the maintainer can determine how each should be handled.

The proposed decoder gives E3 an Unknown state while retaining its raw held value. E5, which lacks the required status, becomes invalid_contract instead. Both remain visible for review; queued and sent keep their ordinary handling. The Python resource executes those mappings over the five invented inputs and checks membership, without contacting a provider. Retaining held is intentionally not an interpretation of held. It keeps the question available for the maintainer instead of allowing a filter to answer it by making the record disappear.

That last limit is useful. Unknown must not be another spelling of queued, failed or successful. The application should block actions whose safety depends on interpreting that state while preserving the record for investigation. Other operations that do not rely on the status may remain possible. A reporting screen can display an unfamiliar value without deciding whether it should retry, cancel or mark a message completed. Representation and action policy need not fail together.

E5 needs another kind of attention. A missing required field may signal a contract failure, whereas Unknown could be a legitimate new provider state. Calling both Unknown obscures that distinction. At the other extreme, rejecting all five records because E3 has a new string can hide unrelated work. There is no universal batch boundary to copy here. Choose how to isolate the problem for the operation being offered, rather than let a deserialiser or collection filter decide which records the operator gets to see.

The old filter hides the records that need attention

Executed decoder over invented API fixtures; held is not attributed to any real provider change.

Scroll sideways to see all columns.

RecordRaw inputOld reportProposed decoding / handling
E1 / v1queuedVisibleKnown queued
E2 / v1sentVisibleKnown sent
E3 / v2heldHiddenUnknown(held); review, no inferred action
E4 / v2queued; reason:nullVisibleKnown queued; retain source
E5 / v2Status absentHiddenInvalid contract; quarantine visibly
Unknown enum and missing required field are different failures. Preserving either record does not authorise retry, cancellation or another status-dependent operation.

Tolerance needs limits as well as a fallback branch

Permitting unknown states is not a reason to ignore unexpected input. Record the raw status, source version and relevant object identity; make the new category observable to its owner. Otherwise the system can accumulate unexamined states indefinitely while claiming compatibility because nothing crashed. The fallback should buy time for a deliberate interpretation, not become a permanent substitute for maintaining the integration. A record that remains review-required deserves a visible unresolved status and a route to resolution.

Display the raw string as data. Do not let it name an executable handler, database column or action. The application can be flexible about representing an unfamiliar value while remaining quite restrictive about what operations follow from it. Otherwise a tolerant decoder merely moves its trust problem into a permissive dispatcher. The example records proposed handling descriptions only; it has no retry, send or cancellation effect. A deployed action path still needs the controlled operation boundary that those descriptions point towards.

There is also a policy choice about extra fields. E4's reason null does not change its known queued status in this example, so the decoder can preserve the payload without inventing a reason. Another endpoint may require a related field for an action, or give null a distinct meaning. Tolerating an additive field is different from assuming all fields are irrelevant forever. Document which inputs drive decisions and which are retained only as source context, then test changes to each category deliberately.

Strict decoding can still be appropriate at particular boundaries. A payment instruction or other consequential command may need to reject anything outside a closed contract. The aim is not universal tolerance. It is to avoid hiding information when the intended operation is to observe or report, while preventing unsupported meanings from driving actions. A durable inbox can retain an unfamiliar payload for later processing even when the current action path refuses it. Different stages can make different, compatible choices about what they accept responsibility for.

Tolerant representation need not permit action

The synthetic E3 held record has different requirements at successive stages.

Follow the labelled arrows. On a narrow screen, scroll sideways if needed.

Tolerant representation need not permit actionDecode E3 as Unknown(raw: held). Keep the unfamiliar string instead of making the record disappear. Make the unresolved state visible. Retain record identity, source version and the unresolved interpretation for investigation. Block status-dependent actions. Until held has a defined meaning, do not treat Unknown as queued, failed or successful merely to continue. From Decode E3 as Unknown(raw: held) to Make the unresolved state visible: Preservation enables investigation. From Make the unresolved state visible to Block status-dependent actions: Visibility does not authorise action.PreservationenablesinvestigationVisibilitydoes notauthoriseactionDecode E3 asUnknown(raw: held)Keep the unfamiliar stringinstead of making the recorddisappear.Make the unresolvedstate visibleRetain record identity,source version and theunresolved interpretation forinvestigation.Block status-dependentactionsUntil held has a definedmeaning, do not treat Unknownas queued, failed orsuccessful merely tocontinue.
E5’s missing required status remains a separate contract failure. The resource proposes handling and performs no send or cancellation.

Historical records do not upgrade themselves

An integration often handles stored old events alongside current responses. Keep the version or representation identity with those records where the provider makes it available. A decoder updated for version two should still be tested against E1 and E2 rather than assume that all input is newly fetched. Replaying an old event through new code can expose assumptions that a happy-path test against the current endpoint never exercises. The fixture deliberately mixes versions in one report for that reason.

Once the provider’s meaning of held is understood, add its documented treatment without pretending the old client knew it all along. Keep the original event and its later interpretation distinguishable. Before rollout, compare which records the new logic makes visible, recategorises or permits an action on. That is a more revealing migration review than checking that both versions parse. It explains what will change for E3 and why the old overview lost it.

Provider version policy must be read as a whole. Alongside ordinary additive changes and planned upgrades, providers may define exceptional security or reliability changes and end-of-support behaviour. GitHub's current policy includes such exceptions and retirement signals.[1] Those details belong in the maintenance plan for that actual integration; they are not grounds to abandon version pinning. They are grounds to assign an owner, monitor provider notices and test migrations before a version choice stops being available.

Use the mixed-version comparison to inspect visibility and action changes together. If the new decoder makes E3 visible but immediately gives it queued’s retry action, it has repaired the report while guessing at the new meaning. If it rejects every old event because a new field is now expected, it has created a replay failure while improving current responses. Keep each case’s expected handling beside its representation version, so a migration review can find either regression before the changed decoder reaches stored records.

An upgrade is complete when the new meaning is usable

Until held has a documented meaning for this endpoint, Unknown: held is an honest way to keep E3 in view. Someone can investigate it without the application guessing whether to retry, cancel or complete it. E5 remains a separate contract question. Those two states give the maintainer specific work to do; a general integration-is-working status would conceal both behind the absence of a crash.

Working on a workflow like this? Hamsa helps finance and operations teams connect their tools and handle the exceptions, starting with one workflow.

See the Payment Operations Pilot

References & further reading

Primary sources behind this article. Living documentation may change.

  1. GitHub: API versioning, additive changes and exceptions
Hamsa Jama

Practical engineering. A human perspective.

More about Hamsa Software and the person behind it. Editorial notes.

A few more things
worth a read.

Back to the reading room
Integrations7 min read

A valid signature still needs a trust decision

An Oprindo sample had a matching asset hash and a valid signature, alongside an untrusted credential result. The useful story is in those separate answers.

Integrations6 min read

Should this old instruction still run?

Repairing a consumer makes replay possible. Current state, dependencies and recovery admission decide whether it is still appropriate.

Integrations7 min read

The backfill arrived after the live update

Historical work needs a population boundary, revision policy and crash-safe checkpoint strategy before its final page can mean done.