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
errorsandexplaintext 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
| Field | Type | Required | Behaviour |
|---|---|---|---|
nome | string | yes | surrounding 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_min | integer | no | maximum delivery rate per minute for this app. Absent ⇒ 60000. Zero or negative ⇒ 400 rate_por_min deve ser positivo |
sandbox | boolean | no | true 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_keyandreceipt_keyappear once, here. No route hands them back. Store both in your vault before you close the response.receipt_keyis the secret that proves delivery receipts. It is stored encrypted on our side and never leaves the server after this response.criado_emis ISO 8601 with an offset.
Errors
| Status | When |
|---|---|
400 | body is not valid JSON; nome empty; rate_por_min less than or equal to zero |
401 | Authorization missing, or the platform key is wrong |
503 | platform 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
| Status | When |
|---|---|
401 | platform key missing or wrong |
404 | no app with that {id} |
500 | failure while writing the new key |
503 | platform 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.
| Field | What it is |
|---|---|
project_id | the Firebase project |
sender_id | the project’s numeric sender |
app_id | the Android app identifier in Firebase, shaped 1:<number>:android:<hash> |
api_key | the Firebase API key found in google-services.json |
package_name | the Android application package |
ios | reserved 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_keyinside theandroidblock 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
| Status | When |
|---|---|
403 | app paused |
404 | no app with that {id} |
429 | per-IP cap |
503 | database 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
- 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.
- Not storing the key right away.
api_keyandreceipt_keyappear once, in the creation response. There is no route to recover them: only the hash is kept in the database. - Confusing the Firebase
api_keywith the app key. The first is public and comes fromgoogle-services.json. The second starts withpm_live_and must never leave your server. - 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.
- Expecting the error envelope on a malformed
{id}. An{id}that is not a UUID is rejected before the handler, with400and a body outside the envelope. - Expecting
200on creation. Success is201. - Reading
rate_por_minas an API cap. It is a delivery pace per minute, not an HTTP request limit. - Sending the wrong credential and reading the body error. The credential is
checked first: with it wrong, you get
401even if the JSON is broken too. - Expecting iOS data from
firebase_params. Theiosfield is reserved and is alwaysnulltoday.