PushMesh
Sign in Create account
Open section navigation

Index

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 POST pays 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 wantyou need
send over gRPCnothing — just the API key in metadata; works today, with any app
receive confirmations over gRPCswitch 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 same notificationId comes back — no duplicate push. (Tested: it did.)
  • The delivery proof stays the same: query the dispatch over REST (GET /api/v1/notifications/{id}) and watch recebidos count 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:

fieldwhat it is
chaveyours, echoed back — how you match request to response
notification_idthe accepted dispatch id
recipientsthe same recipients as the REST response
errors_jsonthe errors object of a partial 2xx — this is where invalid_player_ids arrives
erro_jsonempty = accepted; otherwise the same REST error envelope, with explain
recusa_httptrue = 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:

  1. Open the stream and send Assinar{ app_id, cursor }. cursor = 0 starts from the pending queue; any other value resumes from the last acknowledged entrega_id.
  2. Receive Evento{ entrega_id, evento, payload_json, canal }. The payload_json is the same body as the webhook (with canal: "grpc"), and entrega_id is the same X-PM-Entrega-Id — if you ever migrate from webhooks to the stream, your idempotency keeps working.
  3. 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 is via='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.