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.
| Environment | Endpoint |
|---|---|
| Production | https://api.ppoppo.com/ext (gRPC, port 443) |
| Sandbox | https://api.sandbox.ppoppo.com/ext (gRPC, port 443) |
| Development | http://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
| Service | Purpose |
|---|---|
ExternalMessageService | Send messages, check status, stream events |
ExternalTemplateService | Create and manage message templates |
ExternalAppService | App management, usage stats, poll results |
Templates
Messages are sent from templates. ExternalTemplateService.CreateTemplate takes a list of components:
| Component type | Purpose |
|---|---|
Text | A text block; {{placeholders}} are filled per recipient via vars |
Divider | A visual separator |
Button | A labelled link (label + url) |
Poll | A 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:
| Plan | Rate limit | Monthly messages |
|---|---|---|
free | 100 req/min | 1,000 |
pro | 600 req/min | 50,000 |
enterprise | negotiated | unlimited |
- 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_EXHAUSTEDwith aretry-after-mshint 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.