PushMesh
Sign in Request access
Open section navigation

Index

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:

  • versao is 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 answered 0.1.0.
  • The SDK version is a different thing, with its own numbering. The React Native package @pushmesh/sdk is currently at 0.7.2. The two numbers do not move together and will never line up by accident — do not infer one from the other.
  • git may 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:

  1. The text of explain.causa and explain.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 structured explain fields (campo, bytes, limite, hash_original, mau…).
  2. 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.
  3. 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

TopicPage
the status table and the retry policyAuthentication and errors
every published capLimits
webhook signature and redeliveryWebhooks
what is accepted as-is from another providerMigrating from another provider