Versioning and stability
What is frozen by contract tests in the PushMesh API, what may change without notice, and how to tell which version is live — without confusing the service version with the SDK version.
Versioning and stability
The question that matters when you trust an API with your user base is not “which version?”, it is “what here can change underneath me?”. This page answers exactly that, and separates promises from implementation details.
The version is in the path
The whole public contract lives under /api/v1. Within v1, changes are
additive: new fields may appear in responses, existing fields never disappear
or change type. Breaking the contract would require a new path.
To see what is live:
curl https://api.pushmesh.io/
{ "servico": "pushmesh", "versao": "0.1.0", "git": "unknown" }
Three honest readings of that response:
versaois the SERVICE version. It comes from the service package and goes up with each release. At the time this page was written, the public instance answered0.1.0.- The SDK version is a different thing, with its own numbering. The React
Native package
@pushmesh/sdkis currently at0.7.2. The two numbers do not move together and will never line up by accident — do not infer one from the other. gitmay come back"unknown". It carries the build commit when the instance was compiled with that information injected; when it was not, the field is present and reads"unknown". It is diagnostics, not contract: do not build logic on it.
What is contract here: the /api/v1 path, and the fact that the root answers
a JSON object with those three fields.
What is frozen by contract test
The project keeps model responses from real calls, and the test suite compares them byte for byte. Changing any of those shapes breaks the test before it reaches production — that is what turns “compatibility” from an intention into a mechanical guarantee.
Frozen this way:
| What is frozen |
|---|
| Successful send response |
| Idempotent replay response |
Partial send (errors as an object) |
Nobody reachable (200 with an empty id) |
| 422 for a reused idempotency key |
| Single-send read |
Read on the free plan (with the recebidos_gate block) |
| Send list |
| Body sent to the Android provider |
| Body sent to the iOS provider |
| Device registration response |
Beyond response shapes, these are locked by test:
The published caps. The requests-per-second numbers have a test of their own,
with the reasoning written alongside it: if someone changes them by accident, the
customer who integrated against the published number starts taking 429s with no
explanation.
The webhook signature. Fixed vector: sha256= prefix, 64 hexadecimal
characters, deterministic, and a different secret produces a different signature.
If it changed, every customer endpoint would start rejecting deliveries as
forged.
The webhook retry curve. 1 s → 10 s → 60 s → 5 min → 15 min, capped.
The canonical serialization of the send body. It is what produces the
idempotency request_hash: object keys sorted recursively, arrays in order,
numbers as they came, send_after never reinterpreted. Changing that function
would make every client take a 422 when retrying an already-used key.
The signed strings behind the proofs. The push receipt proof and the In-App display proof have the exact signature text frozen in the contract — the SDK only echoes, it never computes:
pm_rcpt = hex(HMAC-SHA256(receipt_key, "<notification_id>:<device_uuid>"))[..32]
pm_iam = hex(HMAC-SHA256(receipt_key, "iam:<campaign_id>:<device_uuid>:<package_id>"))[..32]
The iam: prefix is what separates the two universes: a push receipt is never
valid as In-App proof, and vice versa, even with the same key.
The measured payload size. The 3,891-byte budget and the 96 bytes the server injects are checked by test against the real render — the front door cannot “forget” bytes it will add itself later.
The shape of the public Firebase parameters. android comes back null when
unconfigured and ios is a reserved field, always null at this stage — the
contract is born stable so the SDK does not have to change when iOS arrives.
What may change without notice
Be explicit with your code about these three, because they are not contract:
- The text of
explain.causaandexplain.como_corrigir. They exist for a human to read and for your log to keep. Never branch your logic on an error string — use the HTTP status and, where they exist, the structuredexplainfields (campo,bytes,limite,hash_original,mau…). - New fields in responses. They may appear at any time, always additive and,
on the compatibility routes, always in the
pm_*namespace. Your deserializer must ignore unknown fields. - Routes outside
/api/v1. What the dashboard uses to talk to the service is an internal contract and changes with the dashboard. Do not automate anything through it: everything an integration needs is in/api/v1.
Also not contract: infrastructure details (how many instances serve you, the
value of X-Pm-Instance, the root’s git field), the order of fields inside a
JSON object, and the performance of any specific call.
How the service evolves without breaking you
Three internal rules hold up the promises above, and they are worth stating because they explain why you can rely on them:
- Database migrations only add. No published migration is ever edited.
- Extensions are born in
pm_*. The compatibility shape is frozen; everything of ours goes into its own namespace, so it never collides with a field your integration already reads. - Nothing is accepted silently. A field that would change who receives, when they receive, or what the person sees — and that we have not implemented yet — returns a named 400 instead of being ignored. The list is in Migrating from another provider.
Deprecation
When a behaviour has to go, the path is the same as key rotation: overlap window first, cut later. API key rotation is the example already implemented — the previous key stays valid for 24 hours, and rotation never produces a 401.
Service health
curl https://api.pushmesh.io/health
Returns status, versao, git, papel, lider, uptime_s, instancia,
database and cache state, modo_degradado, lideranca, pg_down_ha_s and
fila_mais_antiga_s.
It answers 200 while the service is serving — including with the cache down (the database is the truth; the cache is an accelerator) and through a short database failure. It only drops to 503 when the database has been down for more than 60 seconds or the instance is draining.
See also
| Topic | Page |
|---|---|
| the status table and the retry policy | Authentication and errors |
| every published cap | Limits |
| webhook signature and redelivery | Webhooks |
| what is accepted as-is from another provider | Migrating from another provider |