Sparrow CLI
The sparrow CLI is the fastest way to work with a Sparrow server: push test
events, watch deliveries stream by, receive webhooks on your laptop, and debug
transform templates without a single curl invocation.
Install
Section titled “Install”The easiest way — no Go toolchain needed — is a prebuilt binary. Download the
sparrow-cli archive for your OS/arch from the
latest release,
extract it, and move the sparrow binary onto your PATH:
# e.g. macOS arm64: sparrow-cli-<version>-darwin-arm64.tar.gztar xzf sparrow-cli-*.tar.gzsudo mv sparrow /usr/local/bin/sparrow versionPrebuilt binaries are published for macOS and Linux (amd64 + arm64) and Windows (amd64).
Have a Go toolchain? Install straight from source instead:
go install github.com/sarathsp06/sparrow/satellites/sparrow@latest90-second quickstart
Section titled “90-second quickstart”# 1. Point the CLI at your server (writes ~/.sparrow/config.yaml)sparrow init --url http://localhost:8080
# 2. Receive deliveries locally: registers a temporary webhook,# prints every delivery, deletes the webhook on Ctrl-Csparrow listen --event order.created
# 3. In another terminal: push an event# (the event type is auto-created if it doesn't exist yet)sparrow push order.created -d '{"order_id":"ord_1","amount":42}'The delivery shows up in the listen terminal, signature-verified,
headers and body pretty-printed.
How it’s connected
Section titled “How it’s connected”The CLI is a pure REST client — the same public API you can call with curl,
authenticated with X-API-Key when a key is configured. It holds no state on
the server beyond what the API calls create. Every subcommand maps to plain
endpoints:
| Command | API calls |
|---|---|
init | GET /health (probe), then writes ~/.sparrow/config.yaml locally |
push | POST /v1/consumers/{consumer}/events?event=<name> — the server auto-registers an unknown event type inline, so one call is normally enough; the CLI also retries once itself if it ever sees a 404/422 for an unregistered type |
tail deliveries | GET /v1/consumers/{consumer}/deliveries every 2 s, printing rows not seen before; webhook IDs are resolved to URLs via GET /v1/consumers/{consumer}/webhooks |
listen | POST /v1/consumers/{consumer}/webhooks (temporary registration) … DELETE /v1/consumers/{consumer}/webhooks/{id} on Ctrl-C |
use | POST /v1/consumers/{consumer}/webhooks, then GET + PATCH /v1/consumers/{consumer}/subscriptions/{id} to attach the recipe’s transform template and label filters |
template test | none — renders locally with the same Go template engine the server uses, against a synthetic sample context; never calls the API |
listen is the one command with a receiving side. Its technical path:
1. bind a local TCP port (random unless --port; bind address: --bind, default all interfaces)2. POST /v1/consumers/{consumer}/webhooks url = --public-url, default http://host.docker.internal:<port> → response contains the webhook secret (shown once)3. serve HTTP: for each delivery from Sparrow's worker - verify the Standard Webhooks headers (webhook-id, webhook-timestamp, webhook-signature) against that secret — HMAC-SHA256 over "{id}.{timestamp}.{body}" - unsigned or mis-signed requests get 401 and are never forwarded - request bodies are capped at 5 MiB - pretty-print headers + body, or proxy to --forward and mirror its status4. Ctrl-C → DELETE the temporary webhook--bind controls the listen address: the default is all interfaces (so a
Dockerized server can reach it); use --bind 127.0.0.1 for loopback only.
Because the server delivers to the CLI here, the server must be allowed to
reach your machine: the host.docker.internal default covers a Dockerized
server on the same host (with SPARROW_ALLOW_PRIVATE_NETWORKS=true or the
local evaluation Compose file); use --public-url with a tunnel for a remote
server.
Configuration
Section titled “Configuration”sparrow init writes ~/.sparrow/config.yaml (mode 0600):
server_url: http://localhost:8080api_key: "" # sent as X-API-Key when setconsumer: defaultEvery value can be overridden — precedence is environment > flags > config file:
| Env var | Flag | Config key |
|---|---|---|
SPARROW_URL | --url | server_url |
SPARROW_API_KEY | --api-key | api_key |
SPARROW_CONSUMER | --consumer | consumer |
Commands
Section titled “Commands”sparrow init
Section titled “sparrow init”Probes the server (GET /health) and writes the config file. Interactive when
run in a terminal; fully scriptable with flags. Idempotent — re-running
overwrites the file.
sparrow init --url https://sparrow.example.com --api-key $KEY --consumer paymentssparrow push <event-name>
Section titled “sparrow push <event-name>”Pushes one event occurrence and prints its event ID. If the event type isn’t
registered yet (the server answers 404), the CLI registers it and retries
the push once.
sparrow push order.created -d '{"order_id":"ord_1"}' # inline JSONsparrow push order.created -d @payload.json # from a filesparrow push order.created -l env=prod -l region=eu # labels for label_filterssparrow push order.created --idempotency-key ord_1-created # dedup: same key returns the original event idsparrow events
Section titled “sparrow events”Lists event types with their current version, or shows one type’s schema and sample payload.
sparrow events # every event typesparrow events order.created # one type's detailsparrow events versions order.createdsparrow events export and sparrow events import
Section titled “sparrow events export and sparrow events import”Move event type definitions between environments as one JSON bundle. See Moving Event Types Between Environments.
# export by name, by prefix, or everything (stdout by default)sparrow events export order.created order.shipped -f event-types.jsonsparrow events export --prefix order. > event-types.jsonsparrow events export --all -f event-types.json
# preview, then import ("-f -" reads stdin)sparrow events import -f event-types.json --dry-runsparrow events import -f event-types.jsonThe import prints what each entry does (created, a new version v2→v3,
updated, unchanged), the reasons a schema change is breaking, and each
subscription whose template fails against the new schema. It writes nothing,
and exits non-zero, until every block is acknowledged:
| Flag | Accepts |
|---|---|
--accept-version-mismatch | a bundle exported by a different Sparrow version or format |
--accept-edited | a bundle edited after it was exported |
--allow-breaking | schema changes that are breaking for existing subscriptions |
--pause-affected | also pause the subscriptions whose template fails |
A dry run that would be blocked also exits non-zero, so a promotion job can run
--dry-run as a check.
sparrow tail deliveries
Section titled “sparrow tail deliveries”Polls the deliveries API every 2 seconds and prints each new row:
TIME EVENT STATUS CODE ATTEMPTS URL2026-09-12 14:03:21 9f0c1a2b-... success 200 1/3 https://hooks.example/xsparrow tail deliveries # follow everything in the consumersparrow tail deliveries --status failed # only failuressparrow tail deliveries --once # print the current batch and exit (scripting/CI)Ctrl-C exits cleanly.
sparrow listen
Section titled “sparrow listen”Starts a local HTTP receiver, registers it as a temporary webhook, and pretty-prints every delivery (signature verified against the registration secret via the Standard Webhooks headers). The webhook is deleted on Ctrl-C.
sparrow listen --event order.created --event order.cancelledsparrow listen --event order.created --port 9099sparrow listen --event order.created --bind 127.0.0.1 # loopback onlysparrow listen --event order.created --forward http://localhost:3000/hooks # proxy to your app, mirror its statussparrow listen --event order.created --public-url https://my-tunnel.example # e.g. an ngrok/cloudflared tunnelOnly deliveries signed with the temporary webhook’s secret are accepted;
unsigned or mis-signed requests get 401 and are never forwarded with
--forward. Request bodies are capped at 5 MiB.
--bind controls the listen address (default: all interfaces, so a Dockerized
server can reach it). Use --bind 127.0.0.1 for loopback only.
Local-dev caveats:
- Without
--public-url, the webhook is registered ashttp://host.docker.internal:<port>— this assumes the Sparrow server runs in Docker on the same machine (Docker Desktop resolveshost.docker.internalto your host; the local evaluation Compose file maps it on Linux too). If the server runs directly on the host, pass--public-url http://localhost:<port>. - Sparrow blocks private-network delivery URLs by default (SSRF guard).
For local receivers, start the server with
SPARROW_ALLOW_PRIVATE_NETWORKS=true(the local evaluation Compose file already sets this).
sparrow recipes
Section titled “sparrow recipes”Lists the recipes built into the binary (name, params, description). Supports
-o json|yaml.
sparrow recipessparrow use <recipe>
Section titled “sparrow use <recipe>”Applies a recipe: registers a webhook with the recipe’s URL and headers, then
enables its transform template (plus any --label filters) on the created
subscription(s). All shipped recipes are embedded in the binary, so
sparrow use slack works anywhere. Lookup order: --file, then
./recipes/<name>.yaml, then $SPARROW_RECIPES_DIR/<name>.yaml (local files
override a built-in of the same name), then the built-in catalog.
sparrow use slack --event order.created --param webhook_url=https://hooks.slack.com/services/T00/B00/xxxsparrow use slack --event order.created --label env=prod # only prod-labelled eventssparrow use myrecipe --file ./my-recipe.yaml --event user.signupRecipe params not passed with --param are prompted for interactively. The
{{param "x"}} tokens are substituted client-side before registration; the
transform template itself is rendered server-side per delivery.
sparrow template test <file>
Section titled “sparrow template test <file>”Renders a transform template entirely locally — no network call — with the
same Go template engine the server uses for deliveries, against a synthetic
sample context (event_id: "evt_sample", attempt: 1, current timestamp,
your --payload). It’s the debugging loop for recipe templates before you
apply one:
sparrow template test slack.tmpl --event-name order.created --payload '{"order_id":"ord_1","amount":42}'sparrow template test slack.tmpl --payload @sample-event.jsonPrints the rendered output, or the parse/render error. Like a subscription, it
treats a field the payload lacks as an error; pass --missing-key zero to
render it as <no value> instead. The template context is
{{.event_id}}, {{.event_name}}, {{.timestamp}}, {{.attempt}}, and
{{.payload}}, with helper functions like json and upper
(GET /v1/template-functions lists them all).
A typical recipe-authoring loop:
# 1. iterate locally until the output is destination-shapedsparrow template test satellites/recipes/slack.tmpl --payload @sample.json
# 2. apply the recipesparrow use slack --event order.created --param webhook_url=$SLACK_URL
# 3. push a test event and watch it landsparrow push order.created -d @sample.jsonsparrow tail deliveriessparrow tokens
Section titled “sparrow tokens”Manage access tokens — named, revocable credentials for people, CI, and
services. A tenant-wide token (no --consumer) works exactly like
SPARROW_API_KEY and expires after the server’s default (90 days unless
SPARROW_TOKEN_DEFAULT_TTL changes it) unless --ttl is given; a consumer token
only works through the portal API, limited to that consumer. To give a person
access to the web UI, prefer sparrow invite (next section).
sparrow tokens create
Section titled “sparrow tokens create”sparrow tokens create --name ci-deploysparrow tokens create --name acme-sync --consumer acme --ttl 30dsparrow tokens create --name build-bot --ttl neversparrow tokens create --name alice -o jsonPrints the secret once. Store it now — it is not shown again. Send it as
X-API-Key or Authorization: Bearer. Durations accept Go syntax plus a d
suffix (e.g. 90d, 12h, 15m). Tenant-wide tokens default to the server’s
SPARROW_TOKEN_DEFAULT_TTL (90 days unless changed); pass --ttl never for one
that does not expire (revoke it when no longer needed). Consumer tokens default
to 7 days (max 30) and always expire. sparrow invite --token-ttl takes the
same values.
sparrow tokens list
Section titled “sparrow tokens list”sparrow tokens list # active tokenssparrow tokens list --all # include revoked and expiredsparrow tokens list -o jsonTable columns: ID, NAME, ACCESS, CREATED BY, LAST USED, EXPIRES, STATUS.
sparrow tokens revoke
Section titled “sparrow tokens revoke”sparrow tokens revoke <token-id>The token stops working immediately on this server and within 30 seconds everywhere else. Revoking is permanent and idempotent.
sparrow invite
Section titled “sparrow invite”Invite someone to the web UI (or a consumer’s portal) with a one-time link:
sparrow invite alicesparrow invite "acme support" --consumer acme --ttl 7dsparrow invite bob --ttl 15m --ui-url https://sparrow.example.comsparrow invite carol --token-ttl 90d -o jsonOpening the link creates a named access token for that browser, so nobody
pastes a key into chat. The link works once, expires after --ttl (default
24 hours, max 7 days), and can be cancelled with sparrow invites cancel. With
--consumer, the link opens that consumer’s portal instead of the operator
console. --ui-url defaults to the server URL (right when the server serves
the UI itself); pass the UI’s address when it is
hosted separately.
sparrow invites
Section titled “sparrow invites”List or cancel pending invites:
sparrow invites list # pending invitessparrow invites list --all # include redeemed, cancelled, expiredsparrow invites cancel <invite-id>Table columns: ID, FOR, ACCESS, INVITED BY, EXPIRES, STATUS.
sparrow version
Section titled “sparrow version”Prints the CLI version (set at build time via
-ldflags "-X main.version=...").
Real-World Use Cases
Section titled “Real-World Use Cases”The sparrow CLI isn’t just an administrative tool — it accelerates developer productivity across local development, testing, CI/CD automation, and template engineering.
Scenario 1: Zero-Config Local Webhook Receiver & Debugging
Section titled “Scenario 1: Zero-Config Local Webhook Receiver & Debugging”Goal: Develop a feature on your local machine (http://localhost:3000/api/webhooks) that reacts to user.signup events without deploying to a public server or using third-party tunnels.
# Register a temporary local receiver and forward events to your appsparrow listen --event user.signup --forward http://localhost:3000/api/webhooksHow it works: sparrow listen binds a local port, registers a temporary webhook on the Sparrow server pointing to your machine, verifies incoming Standard Webhooks signatures on the fly (unsigned requests are rejected with 401 and never forwarded), proxies the body to your app, and cleans up the temporary webhook when you hit Ctrl-C. Use --bind 127.0.0.1 to restrict the listener to loopback only.
Scenario 2: CI/CD Pipeline Automation & Deployment Notifications
Section titled “Scenario 2: CI/CD Pipeline Automation & Deployment Notifications”Goal: Trigger downstream workflows or notify monitoring systems whenever a continuous integration build or production release succeeds.
In your GitHub Actions workflow (.github/workflows/deploy.yml):
- name: Notify Sparrow of Successful Deployment env: SPARROW_URL: ${{ secrets.SPARROW_URL }} SPARROW_API_KEY: ${{ secrets.SPARROW_API_KEY }} run: | sparrow push deploy.success \ -d '{"app": "checkout-service", "commit": "'"$GITHUB_SHA"'", "environment": "production"}' \ -l env=prod \ --idempotency-key "deploy-prod-${{ github.run_id }}"Why this helps: Sparrow deduplicates identical idempotency keys, registers the deploy.success event type automatically if it doesn’t exist, and fans out notifications to Slack, PagerDuty, or audit logs based on your subscription label filters.
Scenario 3: Offline Transform Template Prototyping
Section titled “Scenario 3: Offline Transform Template Prototyping”Goal: Draft a complex Go template transform for a partner’s custom webhook schema before applying it to production subscriptions.
# 1. Write your Go template (e.g., custom_partner.tmpl)cat << 'EOF' > custom_partner.tmpl{{ dict "partner_id" "acme" "transaction" (dict "id" .payload.order_id "total_dollars" (div .payload.amount_cents 100)) | json }}EOF
# 2. Render and debug locally against sample payload datasparrow template test custom_partner.tmpl --payload '{"order_id": "ord_9988", "amount_cents": 4995}'Output:
{"partner_id":"acme","transaction":{"id":"ord_9988","total_dollars":49.95}}Next step: Once verified, attach it directly using a single command:
sparrow use partner_recipe --file ./partner-recipe.yaml --event order.completed