PushMesh
Sign in Request access
Open section navigation

Index

Applications and keys

Create an application, rotate its API key, and read the public Firebase parameters — the /api/v1/apps routes.

Apps and keys

An app is PushMesh’s unit of isolation: devices, sends, in-app campaigns and delivery credentials all belong to an app, and nothing crosses the boundary between two of them. The app_id is a UUID; it is the identity your application carries and it never changes.

Every app has an API key — that is what authenticates calls from your server.

Base address: https://api.pushmesh.io

See also: Authentication and errors has the full credential contract and the HTTP status table.

Language of error messages. The service currently returns the errors and explain text in Brazilian Portuguese. Branch on the HTTP status code, never on the message string.


First things first: who creates an app

Two of these three routes — create app and rotate key — require the platform key, which belongs to whoever operates the service, not to the customer.

In practice, if you are a PushMesh customer:

  • Creating the app: in the panel, at https://app.pushmesh.io. On creation, the key is shown once on screen.
  • Rotating the key: also in the panel, on the app’s page.

The reference below exists because the API is public and the contract is the same — and because anyone running PushMesh on their own infrastructure uses exactly these routes. Do not invent a platform key: without it, the first two routes answer 401.

The third route (firebase_params) is different: it is genuinely public, with no credential at all, and the Android SDK is what calls it.


The app key

pm_live_<kid>_<secret>      production
pm_test_<kid>_<secret>      sandbox
  • kid: 8 characters [a-z0-9]. It is the lookup index — it is not a secret.
  • secret: 43 alphanumeric characters, ~256 bits of entropy.

How to use it:

Authorization: Basic pm_live_<kid>_<secret>

Bearer works identically. An HTTP client that only builds classic Basic auth also works: send Basic <base64 of "anything:pm_live_..."> — whatever comes before the colon is ignored.

The raw key is never stored. Only its sha256 lives in the database. That has a practical consequence worth repeating: if you lose the key it cannot be recovered — rotation is the only way out.

Key resolution is cached for 60 seconds. A revoked key stops working in under a second, because revocation clears the caches; a key that expired on the rotation clock may keep being accepted for up to a minute past its deadline.


POST /api/v1/apps — create an app

Authentication: platform key.

Authorization: Bearer <PLATFORM_KEY>

The comparison runs in constant time — guessing the key by measuring response time does not work. Every rejected attempt is recorded in the audit trail, without the attempted key: at most we keep the kid, and only when the presented credential has the pm_* shape.

curl -X POST https://api.pushmesh.io/api/v1/apps \
  -H "Authorization: Bearer PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "nome": "My application",
    "rate_por_min": 60000,
    "sandbox": false
  }'

Fields

FieldTypeRequiredBehaviour
nomestringyessurrounding whitespace is trimmed. Empty (or whitespace only) ⇒ 400 nome não pode ser vazio. It is a panel label: it never reaches the device and is not part of any key
rate_por_minintegernomaximum delivery rate per minute for this app. Absent ⇒ 60000. Zero or negative ⇒ 400 rate_por_min deve ser positivo
sandboxbooleannotrue produces a key prefixed pm_test_; absent or false produces pm_live_

What rate_por_min means: it is the pace at which the service hands this app’s notifications to the providers (Google and Apple), honoured by the dispatcher. It is not an HTTP request cap on the API.

Response

201 Created

{
  "id": "0198f3c1-4a2b-7c3d-8e9f-0a1b2c3d4e5f",
  "nome": "My application",
  "api_key": "pm_live_a1b2c3d4_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "kid": "a1b2c3d4",
  "receipt_key": "…64 hex characters…",
  "rate_por_min": 60000,
  "criado_em": "2026-08-27T14:00:00+00:00"
}
  • api_key and receipt_key appear once, here. No route hands them back. Store both in your vault before you close the response.
  • receipt_key is the secret that proves delivery receipts. It is stored encrypted on our side and never leaves the server after this response.
  • criado_em is ISO 8601 with an offset.

Errors

StatusWhen
400body is not valid JSON; nome empty; rate_por_min less than or equal to zero
401Authorization missing, or the platform key is wrong
503platform key not configured on the instance; secret vault not configured; database unavailable

Order matters: the credential is checked before the body. Broken JSON sent without a credential returns 401, not 400.


POST /api/v1/apps/{id}/rotate_key — replace the key

Authentication: platform key.

curl -X POST https://api.pushmesh.io/api/v1/apps/APP_ID/rotate_key \
  -H "Authorization: Bearer PLATFORM_KEY" \
  -H "Content-Type: application/json" \
  -d '{"sandbox": false}'

The body is optional. sandbox: true makes the new key come out with the pm_test_ prefix; absent or false gives pm_live_. The prefix changes nothing else: it is a marker so you can tell environments apart.

Response

200 OK

{
  "id": "0198f3c1-4a2b-7c3d-8e9f-0a1b2c3d4e5f",
  "api_key": "pm_live_e5f6a7b8_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
  "kid": "e5f6a7b8",
  "anterior_expira_em": "2026-08-28T14:00:00+00:00"
}

The 24-hour window

Rotation never produces a 401. The previous key does not die the instant you rotate: it is given a 24-hour validity and keeps being accepted until then. The anterior_expira_em field states exactly when it stops working (null if there was no previous key).

What this means: you rotate, update your services calmly over the day, and no in-flight call breaks. After 24 hours the old key answers 401 chave inexistente, revogada ou expirada.

Add the 60-second resolution cache to that deadline: right at the cutover, a call with the old key may still pass for up to a minute.

Errors

StatusWhen
401platform key missing or wrong
404no app with that {id}
500failure while writing the new key
503platform key not configured on the instance; database unavailable

An {id} that is not a valid UUID is rejected before the handler, with 400 and a body that does not follow the standard error envelope.


GET /api/v1/apps/{id}/firebase_params — public Firebase parameters

Authentication: none. This route is public by design.

The customer pastes google-services.json into the panel once, and the Android application no longer needs the file bundled in: the SDK fetches the values here, at boot. How to obtain that file — and the credential that actually sends — is in Delivery credentials.

Why it can be public: the five values returned already ship inside every published APK. They identify the Firebase project; they do not authorise sending push. They are not a credential, so they are not treated as one.

curl https://api.pushmesh.io/api/v1/apps/APP_ID/firebase_params

Response

200 OK, when the app has parameters configured:

{
  "android": {
    "project_id": "my-firebase-project",
    "sender_id": "123456789012",
    "app_id": "1:123456789012:android:abcdef0123456789",
    "api_key": "AIza…",
    "package_name": "com.example.myapp"
  },
  "ios": null
}

When they have not been configured, the same 200 with "android": null.

FieldWhat it is
project_idthe Firebase project
sender_idthe project’s numeric sender
app_idthe Android app identifier in Firebase, shaped 1:<number>:android:<hash>
api_keythe Firebase API key found in google-services.json
package_namethe Android application package
iosreserved field. Always null in this phase — the shape is stable from day one so the SDK does not have to change when iOS arrives

Mind the name. The api_key inside the android block is the public Firebase key from Google’s file. It is not your PushMesh app key (pm_live_…). They are different things with similar names, and confusing the two is the most expensive mistake available here.

Limit

This route shares the device routes’ cap: 120 requests per minute per source IP, fixed 60-second window. Exceed it and you get 429 with Retry-After: 60.

Errors

StatusWhen
403app paused
404no app with that {id}
429per-IP cap
503database unavailable

Note the difference: here an unknown app_id returns 404. On the device routes the same case returns 400 app_id inexistente, because there app_id is a body field rather than the URL’s resource.


First-integration pitfalls

  1. Trying to create the app through the API as a customer. Creating an app and rotating a key require the platform key, which stays with whoever operates the service. Customers create and rotate in the panel.
  2. Not storing the key right away. api_key and receipt_key appear once, in the creation response. There is no route to recover them: only the hash is kept in the database.
  3. Confusing the Firebase api_key with the app key. The first is public and comes from google-services.json. The second starts with pm_live_ and must never leave your server.
  4. Assuming rotation kills the old key immediately. It stays valid for another 24 hours, deliberately, so the cutover breaks nothing in flight. If your requirement is to cut access right now, rotation is not the tool.
  5. Expecting the error envelope on a malformed {id}. An {id} that is not a UUID is rejected before the handler, with 400 and a body outside the envelope.
  6. Expecting 200 on creation. Success is 201.
  7. Reading rate_por_min as an API cap. It is a delivery pace per minute, not an HTTP request limit.
  8. Sending the wrong credential and reading the body error. The credential is checked first: with it wrong, you get 401 even if the JSON is broken too.
  9. Expecting iOS data from firebase_params. The ios field is reserved and is always null today.