Ppoppo Docs

Messaging API

The Messaging External API lets your server send notifications to Ppoppo users over gRPC. This page is the reference; for an end-to-end walkthrough (template → send → consent → delivery) see the bulk-messaging guide.

EnvironmentEndpoint
Productionhttps://api.ppoppo.com/ext (gRPC, port 443)
Sandboxhttps://api.sandbox.ppoppo.com/ext (gRPC, port 443)
Developmenthttp://localhost:3203 (gRPC)

Authentication

Every RPC requires a JWT Bearer token in gRPC metadata. Obtain it through the OAuth2 client_credentials grant against the token endpoint, using your External API client_id + client_secret (distinct from the PKCE login client):

authorization: Bearer <JWT>

The pcs-external crate fetches and refreshes this token for you. With raw gRPC stubs, request it yourself and set the metadata.

Services

ServicePurpose
ExternalMessageServiceSend messages, check status, stream events
ExternalTemplateServiceCreate and manage message templates
ExternalAppServiceApp management, usage stats, poll results

Templates

Messages are sent from templates. ExternalTemplateService.CreateTemplate takes a list of components:

Component typePurpose
TextA text block; {{placeholders}} are filled per recipient via vars
DividerA visual separator
ButtonA labelled link (label + url)
PollA poll — see the Poll API

CreateTemplate returns the template's id (ULID, e.g. tpl_01HQ…), name, is_active, version, and created_at.

The minimum viable template is a single Text component with one placeholder — e.g. [{"type":"text","content":"{message}"}]. Your app formats the string and passes it as vars: {"message": "Order #123 confirmed."}.

Send a message

The send hot-path is covered by the curated pcs-external crate — its SendOnly scope exposes send_alert, which handles the token fetch/refresh and the gRPC call:

use pcs_external::{PcsExternalClientBuilder, scopes::SendOnly};
use pcs_external::types::{Ppnum, RecipientList, TemplateId};

let client = PcsExternalClientBuilder::new(api_url, token_url, client_id, client_secret)
    .build::<SendOnly>()
    .await?;
let recipients = RecipientList::from_ppnums(vec![Ppnum::try_new("12345678901")?])?;
let outcome = client.send_alert(&TemplateId::new("tpl_…"), &recipients, None).await?;

send_alert returns a SendOutcome (id, state, total_recipients) — an aggregate acceptance receipt, not per-recipient delivery status. For per-recipient outcomes, poll GetSendRequestStatus or stream delivery events.

Per-recipient templating (vars) and bulk sends (up to 1,000 recipients per request) use the generated-stub CreateSendRequest path — see the bulk-messaging guide.

Check send status

ExternalMessageService.GetSendRequestStatus returns a summary (delivered, pending_consent, failed) plus a per-recipient breakdown keyed by ppnum.

Rate limits and quotas

All External API RPCs share a per-app rate limit and a monthly message quota tied to your plan:

PlanRate limitMonthly messages
free100 req/min1,000
pro600 req/min50,000
enterprisenegotiatedunlimited
  • The rate limit is a sliding window per app, shared across all RPCs.
  • The monthly quota decrements only on CreateSendRequest; read-only RPCs don't consume it.
  • On exhaustion the status is RESOURCE_EXHAUSTED with a retry-after-ms hint in trailing metadata.
  • The quota resets at the start of each calendar month (UTC).

Errors

gRPC status codes are listed in the shared Errors reference.