Skip to content

Repository files navigation

dwolla CLI

The official Dwolla command-line interface — explore the API, seed sandbox data, and automate workflows from your terminal.

Beta. The Dwolla CLI is in public beta. Commands and flags are stable but may still change ahead of v1. Please share feedback.

Installation

Install via script:

# macOS / Linux
curl -fsSL https://raw.githubusercontent.com/dwolla/dwolla-cli/v0.1.0-beta.1/scripts/install.sh | bash

# Windows (PowerShell)
irm https://raw.githubusercontent.com/dwolla/dwolla-cli/v0.1.0-beta.1/scripts/install.ps1 | iex

Each URL points at a published release tag. The script downloads the latest release for your platform and verifies its SHA-256 against the release checksums.txt, aborting on any mismatch.

With Go installed:

go install github.com/dwolla/dwolla-cli/cmd/dwolla@latest

Manual: download the archive for your platform from GitHub Releases and extract the dwolla binary onto your PATH.

On macOS, until the binary is code-signed you may need to clear the quarantine attribute after a manual download: xattr -d com.apple.quarantine ./dwolla.

Build from source:

git clone https://github.com/Dwolla/dwolla-cli.git
cd dwolla-cli
go build -o dwolla ./cmd/dwolla
./dwolla --help

Requires Go 1.25+.

Quick start

export DWOLLA_CLIENT_ID=<your-sandbox-client-id>
export DWOLLA_CLIENT_SECRET=<your-sandbox-client-secret>
export DWOLLA_TOKEN_URL=https://api-sandbox.dwolla.com/token

./dwolla auth whoami                 # confirm credentials and environment
./dwolla root get                    # fetch your account root
./dwolla workflows run send-money    # run an end-to-end sandbox scenario
./dwolla trigger transfer_completed  # fire a real sandbox webhook event
./dwolla listen --print              # receive webhooks over a public tunnel (Ctrl+C to stop)

The CLI defaults to the sandbox environment. Set DWOLLA_ENV=production to target production. Prefer the keychain? Run ./dwolla auth login instead of exporting the variables above.

Authentication

Credentials are resolved in priority order: command flag → environment variable → OS keychain → config file (~/.config/dwolla/config.yaml).

# Environment variables (recommended for testing)
export DWOLLA_CLIENT_ID=...
export DWOLLA_CLIENT_SECRET=...
export DWOLLA_TOKEN_URL=https://api-sandbox.dwolla.com/token

# Or store in the OS keychain
./dwolla auth login

# Inspect what's currently configured and where it came from
./dwolla auth whoami

# Clear stored credentials
./dwolla auth logout

API commands

Every Dwolla API resource maps to a command group. Common pattern:

./dwolla <resource> <action> [flags]

Resource groups

Command Aliases Description
root Root account info
customers Create, list, update customers
funding-sources fs Manage bank accounts
transfers Initiate and track transfers
mass-payments mp Bulk payment operations
events Webhook event log
webhook-subscriptions ws Manage webhook endpoints
webhooks Inspect and retry deliveries
sandbox-simulations ss Simulate bank transfer processing
accounts Master account operations
documents Identity document uploads
labels Customer balance labels
beneficial-owners bo UBO management
exchanges Open Banking exchange integrations
exchange-partners ep Available exchange partners
exchange-sessions es Exchange session management
business-classifications bc Business classification lookup
tokens Application access tokens
kba Knowledge-based authentication

Examples

# List customers
./dwolla customers list

# Create a customer (interactive prompt)
./dwolla customers create

# Get a specific transfer
./dwolla transfers get --transfer-id <id>

# List recent events
./dwolla events list --limit 10

# Simulate bank transfer processing (sandbox only)
./dwolla sandbox-simulations simulate

Workflows

Workflows run multi-step sandbox scenarios with a single command, chaining resource IDs and HAL links between steps automatically. Emails are automatically timestamped so repeated runs never collide.

# List all built-in workflows (grouped by tier)
./dwolla workflows list

# See flags for a specific workflow
./dwolla workflows list --slug send-money

# Run with default flags
./dwolla workflows run send-money

# Pass workflow-specific flags
./dwolla workflows run send-money --customer-type business-verified --rail ach-same-day
./dwolla workflows run facilitate-payment --fee 2.50 --charge-to receiver

# Preview the step plan without making any API calls
./dwolla workflows run send-money --dry-run

# Patch a specific step parameter
./dwolla workflows run send-money --override receiver:firstName=Alice

# Run a custom workflow file
./dwolla workflows run ./my-workflow.json

Built-in workflows

Slug Tier Description
send-money Payment Onboard a customer and send funds to your account balance
receive-money Payment Receive funds from a customer's verified bank
me-to-me Payment Move funds between two of a customer's own bank accounts
facilitate-payment Payment Marketplace: payer → receiver with a platform facilitator fee
onboard-receive-only Onboarding Create a receive-only customer
onboard-unverified Onboarding Create an unverified customer, optionally with a bank (--with-bank=false to skip)
onboard-personal-verified Onboarding Create a personal-verified customer with a verified bank
onboard-business-verified Onboarding Create a business-verified customer (controller, beneficial owner, certification)
seed-sandbox Setup Populate a fresh sandbox with all four customer types and a sample transfer

Custom workflows

Workflow files are JSON with a steps array. Each step is one API call; later steps reference earlier results with ${stepName:field}.

{
  "_meta": {
    "template_version": 1,
    "name": "my-workflow",
    "description": "Create a customer and attach a bank."
  },
  "steps": [
    {
      "name": "customer",
      "method": "POST",
      "path": "/customers",
      "headers": { "Idempotency-Key": "${.uuid}" },
      "params": {
        "firstName": "Ada",
        "lastName": "Lovelace",
        "email": "ada+${.timestamp}@example.com",
        "type": "personal",
        "address1": "123 Main St",
        "city": "Des Moines",
        "state": "IA",
        "postalCode": "50309",
        "dateOfBirth": "1990-01-01",
        "ssn": "1234"
      }
    },
    {
      "name": "bank",
      "method": "POST",
      "path": "/customers/${customer:id}/funding-sources",
      "headers": { "Idempotency-Key": "${.uuid}" },
      "params": {
        "routingNumber": "222222226",
        "accountNumber": "100200300",
        "bankAccountType": "checking",
        "name": "Ada Checking"
      }
    }
  ],
  "env": {
    "CUSTOMER_ID": "${customer:id}",
    "BANK_URL":    "${bank:location}"
  }
}

Variable reference syntax:

Form Resolves to
${stepName:id} Resource ID extracted from Location header or _links.self.href
${stepName:location} Full Location header URL
${stepName:dotted.path} JSONPath into the step's response body
${stepName:dotted.path[@key=val].field} Inline array filter
${.timestamp} Unix seconds at workflow start (use in emails to avoid duplicates)
${.uuid} Fresh random UUID per step (use as idempotency key)
${.env:VAR|default} Environment variable with optional fallback

See examples/ for full working workflow files.

Trigger

trigger fires a real Dwolla sandbox webhook event by creating the necessary resources and, where required, running simulation passes. Useful for testing webhook handlers without manual setup.

# List available events
./dwolla trigger --list

# Fire an event
./dwolla trigger customer_created
./dwolla trigger transfer_completed    # creates resources + 2 simulation passes
./dwolla trigger transfer_failed       # R01 bank → 2 simulation passes → failure event

Available events

Event What it does
customer_created Creates a receive-only customer
customer_verified Creates a personal-verified customer (auto-passes KYC in sandbox)
funding_source_added Creates a customer and attaches a bank account
funding_source_verified Attaches a bank and completes micro-deposit verification
transfer_created Creates a customer, bank, and initiates a transfer
transfer_completed Initiates a transfer and simulates it to completion (2 passes)
transfer_failed Initiates a transfer with an R01 bank and simulates it to failure (2 passes)

Triggers are sandbox-only and refuse to run against production credentials.

Listen

listen opens a public tunnel, registers a Dwolla webhook subscription pointing at it, and either forwards incoming events to a local URL, prints them to stdout, or both. The subscription is automatically removed when you exit (Ctrl+C).

# Forward webhooks to your local app
dwolla listen --forward-to localhost:3000/webhooks

# Print payloads to stdout — no local app needed
dwolla listen --print

# Forward AND print (useful for debugging alongside your app)
dwolla listen --forward-to localhost:3000/webhooks --print

# Filter to specific event topics only
dwolla listen --forward-to localhost:3000/webhooks --events transfer_completed,customer_verified

# Force a specific tunnel provider
dwolla listen --forward-to localhost:3000/webhooks --tunnel localtunnel

Tunnel providers

listen uses Cloudflare Quick Tunnels by default — no Cloudflare account or domain ownership required. The tunnel URL looks like https://random-name.trycloudflare.com.

Cloudflare requires the cloudflared binary to be in your PATH. If it is not found, the CLI will prompt you to download and install it automatically (~14 MB, from github.com/cloudflare/cloudflared). The installed binary goes to ~/.local/bin/cloudflared on macOS/Linux or %LOCALAPPDATA%\cloudflared\cloudflared.exe on Windows. You can also install it yourself beforehand:

# macOS
brew install cloudflared

# Linux
# See https://pkg.cloudflare.com/ for package installs, or download directly:
# https://github.com/cloudflare/cloudflared/releases

If cloudflared is unavailable and you decline the download (or the command is running non-interactively), listen falls back to localtunnel.me — a pure Go fallback with no binary dependency. The tunnel URL looks like https://random-name.loca.lt.

Use --tunnel to force a specific provider:

Value Behavior
(default) Cloudflare if available, prompt to download if not, fall back to localtunnel
cloudflare Cloudflare only — error if cloudflared is not installed
localtunnel localtunnel.me only — skips the download prompt

How it works

  1. A random HMAC secret is generated for the session.
  2. A tunnel is opened and a webhook subscription is registered at the tunnel URL with that secret.
  3. Each incoming webhook is verified against the X-Request-Signature-SHA-256 header — requests with a missing or invalid signature are rejected with 401.
  4. Matching events are forwarded to --forward-to (if set) and/or printed to stdout (if --print is set). Filtered-out events are logged as skipped.
  5. On exit, the subscription is deleted. If a previous session exited uncleanly, any orphaned subscriptions (from either provider) are cleaned up automatically on the next startup.

Testing with dwolla trigger

listen and trigger are designed to work together. Open two terminals:

Terminal A — start listening:

dwolla listen --print

Terminal B — fire a real sandbox event:

dwolla trigger transfer_completed

Dwolla delivers the webhook to the tunnel URL, which the CLI verifies and prints to Terminal A. Press Ctrl+C in Terminal A when done — the subscription is removed automatically.

Flags

Flag Description
--forward-to <url> Local URL to receive forwarded webhooks (e.g. localhost:3000/webhooks)
--print Print each webhook payload to stdout
--events <csv> Comma-separated list of event topics to forward/print (default: all)
--port <int> Local port for the tunnel receiver (default: auto-assigned)
--tunnel <provider> Tunnel backend: cloudflare (default) or localtunnel

At least one of --forward-to or --print is required.

listen is sandbox-only and refuses to run against production credentials.

Output formats

All commands support multiple output formats via --output-format (-o):

./dwolla customers list -o json
./dwolla customers list -o yaml
./dwolla customers list -o table
./dwolla customers list -o pretty    # default

Filter output with --jq:

./dwolla customers list -o json --jq '.[].id'
./dwolla workflows list -o json --jq '.[].name'

Global flags

Flag Description
--client-id OAuth2 client ID
--client-secret OAuth2 client secret
--token-url OAuth2 token endpoint
--server-url Override API base URL
--output-format, -o Output format: pretty, json, yaml, table, toon
--jq, -q Filter output with a jq expression
--debug, -d Log full request/response to stderr
--dry-run Print the request without executing it
--no-interactive Disable interactive prompts
--timeout HTTP timeout (e.g. 30s, 5m)

Interactive explorer

./dwolla explore

Browse all commands interactively without memorizing syntax.

Agent mode

When the CLI detects it's running inside an AI coding agent (Claude Code, Cursor, Codex, and others), it automatically switches to agent mode: token-efficient TOON output, structured JSON errors with recovery hints, and no interactive prompts.

CLAUDE_CODE=1 ./dwolla workflows list          # TOON output
FORCE_AGENT_MODE=1 ./dwolla customers list      # force it on anywhere
CLAUDE_CODE=1 ./dwolla customers list --agent-mode=false   # force it off

See docs/agent-mode.md for the full list of detected environments, structured error types, and recommended patterns.

Troubleshooting

Symptom Fix
authentication failed (401) Run ./dwolla whoami to see which credentials/environment are active. Confirm DWOLLA_CLIENT_ID/DWOLLA_CLIENT_SECRET and that DWOLLA_TOKEN_URL points at the right environment.
Commands hit production unexpectedly The CLI defaults to sandbox. Check DWOLLA_TOKEN_URL / DWOLLA_ENV; trigger and listen refuse to run against production.
listen can't start a Cloudflare tunnel Install cloudflared (brew install cloudflared) or let the CLI download it, or force the fallback with --tunnel localtunnel.
422/400 on a mutating command Re-run with --dry-run to inspect the request body before sending.
macOS won't run the downloaded binary xattr -d com.apple.quarantine ./dwolla (code signing is pending).
Need to see the raw request/response Add --debug — auth headers are masked.

Documentation

Regenerate the command reference after changing commands:

go run ./cmd/gendocs docs/commands.md

Feedback

The Dwolla CLI is in public beta. Please share feedback, bugs, or suggestions by opening an issue on this repo.

License

Licensed under the Apache License, Version 2.0. See LICENSE for the full text.

About

Official CLI for the Dwolla by NMI API. Explore endpoints, seed sandbox data, test webhooks, and run multi-step payment workflows from your terminal.

Topics

Resources

Contributing

Stars

0 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages