Send push notifications through a Faro server.
Build a client for one app, then send. Payloads are plain string-keyed maps in the wire
shape, and FaroEx.Audience supplies helpers that keep tag audiences readable.
client = FaroEx.client(base_url: "https://push.example.com", api_key: System.fetch_env!("FARO_KEY"))
FaroEx.send(client,
title: "Flash sale",
body: "50% off for the next 3 hours",
audience: %{
"include" => [
FaroEx.Audience.users(["42", "43"]),
FaroEx.Audience.tag("plan", "pro")
]
}
)
#=> {:ok, %{notification: %{...}, delivery: %{"sent" => 12, ...}}}Failures are returned, never raised
{:error, %FaroEx.Error{}} carries the HTTP status and the server's own error code.
A transport failure arrives as status: nil, code: "transport_error".
No retries, by design
Faro has no idempotency key on its send endpoint, so a blind retry after a timeout
pushes the same notification to real phones twice. Nothing here retries — configure
retries in your own code only if you can accept a duplicate.
Summary
Functions
Builds a client for one Faro app.
Lists the deliveries of one notification.
Reads a notification back, with the deliveries frozen for it.
Sends a notification, returning what was accepted and what has been delivered so far.
Functions
@spec client(keyword()) :: FaroEx.Client.t()
Builds a client for one Faro app.
Options
:base_url(required) — origin of the Faro server, e.g."https://push.example.com".:api_key(required) — an app API key,"<appId>.<secret>". Mint one with thenotifications:readandnotifications:writescopes; this library does nothing else.:connect_timeout— milliseconds, default 5000.:receive_timeout— milliseconds, default 120000. A send blocks for the server's inline drain, which is 10–20 seconds for a ~1000-user audience, so this is deliberately generous.
The app id is read out of the token, so a key and an app id can never disagree. Raises
ArgumentError for a token that is not an app API key — an operator token or a public
client key (yk_…) fails here, at boot, rather than as a 403 at 3am.
@spec deliveries(FaroEx.Client.t(), String.t(), keyword()) :: {:ok, [map()]} | {:error, FaroEx.Error.t()}
Lists the deliveries of one notification.
Options
:status—"pending","sending","sent","failed","invalid"or"skipped".:limit— 1 to 1000, default 200 server-side.
@spec get_notification(FaroEx.Client.t(), String.t()) :: {:ok, %{notification: map() | nil, deliveries: [map()]}} | {:error, FaroEx.Error.t()}
Reads a notification back, with the deliveries frozen for it.
This is how you learn the real outcome: a send returns a snapshot, and the worker keeps
finishing retries and delayed sends in the background. deliveries is the list the API
returns (up to 200 rows); returns {:error, %FaroEx.Error{code: "not_found"}} for an
unknown id.
@spec send(FaroEx.Client.t(), keyword()) :: {:ok, %{notification: map() | nil, delivery: map() | nil}} | {:error, FaroEx.Error.t()}
Sends a notification, returning what was accepted and what has been delivered so far.
Options
:title,:subtitle,:body,:data,:priority,:ttl,:url,:icon,:image,:launch_url,:channel_id,:collapse_id,:send_after_seconds,:audience— the notification content as the API defines it.:datais an arbitrary map handed to the client app;:audienceis an audience map (seeFaroEx.Audience). Options you leave out are not sent.
Returns
{:ok, %{notification: %{...}, delivery: %{"sent" => 12, ...}}}Both maps are the server's JSON, decoded with string keys. The counts are a snapshot:
retries of provider hops finish in the background, so a "pending" count above zero
means there is more to come — poll get_notification/2 for the final tally.
A send accepted with zero deliveries (a user with no device) is {:ok, _}: the
request landed, and an empty audience is not an error.
This call blocks for the server's inline drain — roughly 10–20 seconds for a
~1000-user audience — so run it under your own supervisor
(Task.Supervisor.async_nolink/3) rather than inside a bare request cycle.