Sending and confirmations over gRPC
The platform's second door — high-frequency sends as frames over a single connection, and delivery confirmations as an acked stream instead of webhooks. Same contract, same key, different transport.
Sending and confirmations over gRPC
REST covers most integrations. This page is for the two extremes where it starts to hurt:
- High-frequency individual sends — transactional, one push per event in
your system, thousands per minute. Every
POSTpays handshake, headers and JSON per trip; over gRPC, requests become frames on an HTTP/2 connection that is already up. - Confirmations as a continuous flow — instead of receiving webhooks (and maintaining a public endpoint, with retries and signatures), you open one stream and delivery confirmations come down through it, with acks and cursor-based resume.
Both doors speak the same contract as REST — same dispatch body, same error envelope, same API key — and they are independent choices: you can send over gRPC and receive confirmations by webhook, or the other way around.
What gRPC does not change: delivery to the device is still APNs/FCM, and the delivery receipt is still the same HMAC proof as always. The gain is in acceptance and in the transport of confirmations — there is no magic in the last mile, and you should distrust anyone promising otherwise.
The endpoint
grpc.pushmesh.io:443 (TLS · HTTP/2 · no third-party proxy in front)
Authentication on every call, in metadata — the same key as REST:
authorization: Basic pm_live_<kid>_<secret>
The key is revalidated on every request inside the stream: revoke it in the panel and the stream is cut on the next request — not on the next reconnect.
The contract lives in proto/confirmacoes.proto, package
pushmesh.confirmacoes.v1, and it is freezable: fields are only ever
added, never renumbered or renamed. Ask support for the file or copy it from
the documentation — it is the law of this door.
The two doors are independent — read this first
The grpc mode in the confirmation settings controls only how confirmations
reach you. It is NOT a prerequisite for sending:
| you want | you need |
|---|---|
| send over gRPC | nothing — just the API key in metadata; works today, with any app |
| receive confirmations over gRPC | switch the app’s confirmation mode to grpc |
Any combination is valid: send over gRPC and confirm by webhook, send over REST and confirm by stream — choose per flow, not per bundle.
Start in five minutes (actually tested)
The example below was executed against production before it entered this page
— the acceptance, the push on the device and the delivery receipt actually
happened. You need grpcurl (or any gRPC client) and the
confirmacoes.proto file from this documentation, saved in the current
directory.
python3 - <<'END' | grpcurl -proto confirmacoes.proto \
-H 'authorization: Basic pm_live_YOUR_KID_YOUR_SECRET' \
-d @ grpc.pushmesh.io:443 pushmesh.confirmacoes.v1.Envio/Enviar
import json
body = {
"app_id": "YOUR_APP_ID",
"headings": {"en": "Sent over gRPC"},
"contents": {"en": "The same body as REST, through another pipe"},
"include_player_ids": ["PLAYER_ID"],
}
print(json.dumps({"chave": "my-request-1", "notificacao_json": json.dumps(body)}))
END
The response that comes back, on the same connection:
{
"chave": "my-request-1",
"notificationId": "01a05e29-587e-7e73-b3f4-3df343cfb659",
"recipients": 1
}
Two behaviours worth proving next, because they are the heart of the contract:
- Idempotency: run the same command again with the SAME
chave. The samenotificationIdcomes back — no duplicate push. (Tested: it did.) - The delivery proof stays the same: query the dispatch over REST
(
GET /api/v1/notifications/{id}) and watchrecebidoscount when the device confirms. The transport changed; the promise did not.
Door 1 — Envio.Enviar: dispatches as a stream
service Envio {
rpc Enviar(stream PedidoEnvio) returns (stream Aceite);
}
message PedidoEnvio {
string chave = 1; // idempotency (mirrors REST's Idempotency-Key)
string notificacao_json = 2; // the SAME body as POST /api/v1/notifications
}
notificacao_json is, byte for byte, the body you would send to
POST /api/v1/notifications — same validation, same errors. You do not learn a
second dispatch format; you learn a second transport.
Each request comes back as an Aceite on the same connection:
| field | what it is |
|---|---|
chave | yours, echoed back — how you match request to response |
notification_id | the accepted dispatch id |
recipients | the same recipients as the REST response |
errors_json | the errors object of a partial 2xx — this is where invalid_player_ids arrives |
erro_json | empty = accepted; otherwise the same REST error envelope, with explain |
recusa_http | true = the refusal came from the server (limits, validation) — treat it as a transport failure, never blame the device |
Idempotency: same chave = same dispatch, as in REST. Reconnected and
resent a batch you were unsure about? The server absorbs it.
Cap: there is a limit on simultaneous streams per app — a leaked key cannot open endless connections. One connection with one stream is the normal design; open more only if you measure the need.
Door 2 — Confirmacoes.Fluxo: confirmations come down a stream
This door replaces webhooks for those who prefer an outbound connection to
a public endpoint. It requires switching the app’s confirmation mode
(panel → Confirmations → mode grpc; the modes are consulta, webhook and
grpc). With the wrong mode, the door itself explains what to switch — errors
teach, like everything here.
service Confirmacoes {
rpc Fluxo(stream DoCliente) returns (stream Evento);
}
The protocol in three steps:
- Open the stream and send
Assinar{ app_id, cursor }.cursor = 0starts from the pending queue; any other value resumes from the last acknowledgedentrega_id. - Receive
Evento{ entrega_id, evento, payload_json, canal }. Thepayload_jsonis the same body as the webhook (withcanal: "grpc"), andentrega_idis the sameX-PM-Entrega-Id— if you ever migrate from webhooks to the stream, your idempotency keeps working. - Acknowledge with
Ack{ entrega_id }— or in batch,Ack{ entrega_ids: […] }. Only the ack marks the event as delivered. Connection dropped? Reconnect with your last acknowledged cursor and whatever was not acked comes down again.
At-least-once semantics, like the webhook: be ready to receive an event
twice on reconnection, and deduplicate by entrega_id.
Events that come down the stream:
delivery.received · delivery.clicked · notification.completed · teste.ping
The teste.ping from the confirmation settings also comes through here when
the mode is grpc — that is how you prove the plumbing end to end before the
first real dispatch.
Receiving confirmations in Node.js (door 2)
A complete consumer of the confirmation stream, with acks — the skeleton most integrations use:
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const def = protoLoader.loadSync('confirmacoes.proto');
const pb = grpc.loadPackageDefinition(def).pushmesh.confirmacoes.v1;
const md = new grpc.Metadata();
md.set('authorization', 'Basic pm_live_YOUR_KID_YOUR_SECRET');
const client = new pb.Confirmacoes('grpc.pushmesh.io:443', grpc.credentials.createSsl());
const flow = client.Fluxo(md);
// 1. subscribe — cursor 0 starts from the pending queue; store your last ack to resume
flow.write({ assinar: { app_id: 'YOUR_APP_ID', cursor: 0 } });
flow.on('data', (ev) => {
const body = JSON.parse(ev.payload_json); // the SAME body as the webhook
console.log(ev.evento, body); // delivery.received, delivery.clicked…
// process BEFORE acking: only the ack marks it delivered — if you crash here,
// the event comes down again on reconnect (at-least-once).
flow.write({ ack: { entrega_id: ev.entrega_id } });
});
flow.on('error', (e) => {
// reconnect with your last acknowledged entrega_id instead of cursor 0
console.error('stream dropped — reconnect with cursor', e.message);
});
Before the first real dispatch, prove the plumbing: the test button in the
confirmation settings sends a teste.ping — it comes down this same stream
when the mode is grpc.
Operational details worth knowing
- HTTP/2 keepalive of 30 s on both sides: dead connections are detected without traffic. Configure your client to accept pings without data.
- Intermediate proxies are the enemy of long-lived streams — our endpoint is direct, with no CDN in front and 1-hour timeouts. If there is a proxy on YOUR side, give it the same breathing room.
- Every confirmation delivered here is stamped
canal='grpc', and every dispatch accepted here isvia='grpc'— the audit trail tells the doors apart, forever.
When to stay on REST
Contract honesty: if you fire batched campaigns a few times a day, gRPC gives you nothing — the gain shows up in high-frequency individual sends and in the continuous flow of confirmations. A simple integration deserves a simple transport.