Skip to content

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.

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:

Terminal window
# e.g. macOS arm64: sparrow-cli-<version>-darwin-arm64.tar.gz
tar xzf sparrow-cli-*.tar.gz
sudo mv sparrow /usr/local/bin/
sparrow version

Prebuilt binaries are published for macOS and Linux (amd64 + arm64) and Windows (amd64).

Have a Go toolchain? Install straight from source instead:

Terminal window
go install github.com/sarathsp06/sparrow/satellites/sparrow@latest
Terminal window
# 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-C
sparrow 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.

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:

CommandAPI calls
initGET /health (probe), then writes ~/.sparrow/config.yaml locally
pushPOST /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 deliveriesGET /v1/consumers/{consumer}/deliveries every 2 s, printing rows not seen before; webhook IDs are resolved to URLs via GET /v1/consumers/{consumer}/webhooks
listenPOST /v1/consumers/{consumer}/webhooks (temporary registration) … DELETE /v1/consumers/{consumer}/webhooks/{id} on Ctrl-C
usePOST /v1/consumers/{consumer}/webhooks, then GET + PATCH /v1/consumers/{consumer}/subscriptions/{id} to attach the recipe’s transform template and label filters
template testnone — 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 status
4. 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.

sparrow init writes ~/.sparrow/config.yaml (mode 0600):

server_url: http://localhost:8080
api_key: "" # sent as X-API-Key when set
consumer: default

Every value can be overridden — precedence is environment > flags > config file:

Env varFlagConfig key
SPARROW_URL--urlserver_url
SPARROW_API_KEY--api-keyapi_key
SPARROW_CONSUMER--consumerconsumer

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.

Terminal window
sparrow init --url https://sparrow.example.com --api-key $KEY --consumer payments

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.

Terminal window
sparrow push order.created -d '{"order_id":"ord_1"}' # inline JSON
sparrow push order.created -d @payload.json # from a file
sparrow push order.created -l env=prod -l region=eu # labels for label_filters
sparrow push order.created --idempotency-key ord_1-created # dedup: same key returns the original event id

Lists event types with their current version, or shows one type’s schema and sample payload.

Terminal window
sparrow events # every event type
sparrow events order.created # one type's detail
sparrow events versions order.created

sparrow 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.

Terminal window
# export by name, by prefix, or everything (stdout by default)
sparrow events export order.created order.shipped -f event-types.json
sparrow events export --prefix order. > event-types.json
sparrow events export --all -f event-types.json
# preview, then import ("-f -" reads stdin)
sparrow events import -f event-types.json --dry-run
sparrow events import -f event-types.json

The 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:

FlagAccepts
--accept-version-mismatcha bundle exported by a different Sparrow version or format
--accept-editeda bundle edited after it was exported
--allow-breakingschema changes that are breaking for existing subscriptions
--pause-affectedalso 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.

Polls the deliveries API every 2 seconds and prints each new row:

TIME EVENT STATUS CODE ATTEMPTS URL
2026-09-12 14:03:21 9f0c1a2b-... success 200 1/3 https://hooks.example/x
Terminal window
sparrow tail deliveries # follow everything in the consumer
sparrow tail deliveries --status failed # only failures
sparrow tail deliveries --once # print the current batch and exit (scripting/CI)

Ctrl-C exits cleanly.

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.

Terminal window
sparrow listen --event order.created --event order.cancelled
sparrow listen --event order.created --port 9099
sparrow listen --event order.created --bind 127.0.0.1 # loopback only
sparrow listen --event order.created --forward http://localhost:3000/hooks # proxy to your app, mirror its status
sparrow listen --event order.created --public-url https://my-tunnel.example # e.g. an ngrok/cloudflared tunnel

Only 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 as http://host.docker.internal:<port> — this assumes the Sparrow server runs in Docker on the same machine (Docker Desktop resolves host.docker.internal to 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).

Lists the recipes built into the binary (name, params, description). Supports -o json|yaml.

Terminal window
sparrow recipes

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.

Terminal window
sparrow use slack --event order.created --param webhook_url=https://hooks.slack.com/services/T00/B00/xxx
sparrow use slack --event order.created --label env=prod # only prod-labelled events
sparrow use myrecipe --file ./my-recipe.yaml --event user.signup

Recipe 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.

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:

Terminal window
sparrow template test slack.tmpl --event-name order.created --payload '{"order_id":"ord_1","amount":42}'
sparrow template test slack.tmpl --payload @sample-event.json

Prints 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:

Terminal window
# 1. iterate locally until the output is destination-shaped
sparrow template test satellites/recipes/slack.tmpl --payload @sample.json
# 2. apply the recipe
sparrow use slack --event order.created --param webhook_url=$SLACK_URL
# 3. push a test event and watch it land
sparrow push order.created -d @sample.json
sparrow tail deliveries

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).

Terminal window
sparrow tokens create --name ci-deploy
sparrow tokens create --name acme-sync --consumer acme --ttl 30d
sparrow tokens create --name build-bot --ttl never
sparrow tokens create --name alice -o json

Prints 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.

Terminal window
sparrow tokens list # active tokens
sparrow tokens list --all # include revoked and expired
sparrow tokens list -o json

Table columns: ID, NAME, ACCESS, CREATED BY, LAST USED, EXPIRES, STATUS.

Terminal window
sparrow tokens revoke <token-id>

The token stops working immediately on this server and within 30 seconds everywhere else. Revoking is permanent and idempotent.

Invite someone to the web UI (or a consumer’s portal) with a one-time link:

Terminal window
sparrow invite alice
sparrow invite "acme support" --consumer acme --ttl 7d
sparrow invite bob --ttl 15m --ui-url https://sparrow.example.com
sparrow invite carol --token-ttl 90d -o json

Opening 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.

List or cancel pending invites:

Terminal window
sparrow invites list # pending invites
sparrow invites list --all # include redeemed, cancelled, expired
sparrow invites cancel <invite-id>

Table columns: ID, FOR, ACCESS, INVITED BY, EXPIRES, STATUS.

Prints the CLI version (set at build time via -ldflags "-X main.version=...").

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.

Terminal window
# Register a temporary local receiver and forward events to your app
sparrow listen --event user.signup --forward http://localhost:3000/api/webhooks

How 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.

Terminal window
# 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 data
sparrow 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:

Terminal window
sparrow use partner_recipe --file ./partner-recipe.yaml --event order.completed