Metadata-Version: 2.5
Name: withfeedback
Version: 0.2.0
Summary: Command-line client for withfeedback.com — testimonials, feedback moderation, imports, widgets, and NPS
Project-URL: Homepage, https://withfeedback.com
Project-URL: Documentation, https://withfeedback.com/api/docs/
License: MIT
Keywords: cli,feedback,testimonials,withfeedback
Requires-Python: >=3.10
Requires-Dist: click>=8.1
Requires-Dist: httpx>=0.27
Requires-Dist: keyring>=24
Description-Content-Type: text/markdown

# withfeedback CLI

Command-line client for [withfeedback.com](https://withfeedback.com):
testimonials, feedback moderation, CSV imports, surveys, NPS, widgets, and
quota usage — from your terminal or CI.

## Install

```bash
pip install withfeedback
# or run without installing:
uvx withfeedback --help
```

The console command is `withfeedback`.

## Quick start with a personal access token (PAT)

Good for CI and quick scripting:

1. Log in at your withfeedback.com instance and open **Account → API Tokens**
   (`/accounts/tokens/`).
2. Create a token with the scopes you need (see the table below).
3. Either export it per-shell:

   ```bash
   export WITHFEEDBACK_TOKEN=spd_abc123...
   withfeedback testimonials list --team <TEAM_ID> --project <PROJECT_ID>
   ```

   or persist it in the OS keychain:

   ```bash
   withfeedback login --token spd_abc123...
   ```

API access requires a plan that includes the API/CLI/MCP feature (Pro and
above).

## Login via OAuth2 device flow (interactive)

```bash
withfeedback login
```

No flags needed against withfeedback.com — the CLI defaults to the official
public client id `7yQMsnY2Is2f5tCuwwgItoQu3fRkEX2wnzIRj0Vh` (public by
design — device-flow clients carry no secret). For a self-hosted instance
pass your own client:

```bash
withfeedback login --client-id <CLIENT_ID>       # or WITHFEEDBACK_CLIENT_ID
```

What happens:

1. The CLI calls `POST /o/device-authorization/` and prints a verification
   URL plus a short code (it also tries to open your browser).
2. You open the URL, enter the code, and approve the requested scopes.
3. The CLI polls `POST /o/token/` until you approve, then stores the access
   and refresh tokens in the **OS keychain** via `keyring` (macOS Keychain,
   Windows Credential Locker, Secret Service/KWallet on Linux) — never in a
   plaintext file.

When an access token expires, the CLI transparently refreshes it. Refresh
tokens rotate on every use; the rotated token replaces the stored one.
(Stored PATs are never "refreshed" — if a PAT is rejected, the CLI tells you
to create a new one and run `withfeedback login --token` again.)

```bash
withfeedback logout   # remove stored tokens from the keychain
```

Token resolution precedence for every command:
`--token` flag > `WITHFEEDBACK_TOKEN` env var > OS keychain.

## Commands

```bash
withfeedback login [--client-id <ID> | --token <PAT>]
withfeedback logout

withfeedback testimonials list    --team <ID> --project <ID> [--status pending|approved|rejected|spam] [--rating N] [--q text] [--json]
withfeedback testimonials approve <SUBMISSION_ID> --team <ID> [--json]
withfeedback testimonials reject  <SUBMISSION_ID> --team <ID> [--json]
withfeedback testimonials create  --team <ID> --project <ID> --text "..." [--rating N] [--name ..] [--email ..] [--title ..] [--company ..] [--consent] [--tag t]... [--json]

withfeedback import csv FILE --team <ID> --project <ID> --text-col COL [--rating-col COL] [--name-col COL] [--email-col COL] [--consent] [--json]

withfeedback surveys list            --team <ID> --project <ID> [--json]
withfeedback surveys create          --team <ID> --project <ID> --name "..." --type nps|csat|ces|pmf|custom [--questions '<JSON>'] [--json]
withfeedback surveys update          <SURVEY_ID> --team <ID> --project <ID> [--name ..] [--thanks-message ..] [--status draft|live|archived] [--json]
withfeedback surveys publish-version <SURVEY_ID> --team <ID> --project <ID> --questions '<JSON>' [--json]
withfeedback nps summary             --team <ID> --project <ID> [--json]

withfeedback widgets list   --team <ID> --project <ID> [--json]
withfeedback widgets create --team <ID> --project <ID> --type wall|carousel|collect_feedback|collect_survey|nps_popover --name "..." [--survey <ID>] [--config '<JSON>'] [--filter '<JSON>'] [--collect-video] [--inactive] [--json]
withfeedback widgets update <WIDGET_ID> --team <ID> --project <ID> [--name ..] [--survey <ID>] [--config '<JSON>'] [--filter '<JSON>'] [--collect-video/--no-collect-video] [--active/--inactive] [--json]
withfeedback widgets delete <WIDGET_ID> --team <ID> --project <ID> [--json]

withfeedback usage          --team <ID> [--json]
```

`--config`, `--filter`, and `--questions` take a JSON string. Survey questions
are edited by publishing a new version (`surveys publish-version`), never in
place; taking a survey live is subject to your plan's live-survey limit.

Human output is plain text tables. Pass `--json` for machine-readable output
(raw API responses) — errors still go to stderr.

### CSV import

`withfeedback import csv` validates the file locally (header columns, row
count), builds the column mapping from your `--*-col` options, and uploads
the file to the server's multipart import endpoint. Rows are processed
asynchronously and land as **pending** submissions; the command prints the
batch id and a status URL to poll.

```bash
withfeedback import csv reviews.csv \
  --team <TEAM_ID> --project <PROJECT_ID> \
  --text-col "Testimonial" --rating-col "Stars" \
  --name-col "Author" --email-col "Email" --consent
```

Pass `--consent` only when display consent was actually collected for these
rows.

## Required scopes per command

| Command | Required scope |
|---|---|
| `testimonials list` | `read:feedback` (or `read:testimonials`) |
| `testimonials approve` / `reject` | `moderate:submissions` (`write:*` cannot moderate) |
| `testimonials create` | `write:testimonials` (or `write:feedback`) |
| `import csv` | `write:feedback` |
| `surveys list`, `nps summary` | `read:surveys` |
| `surveys create` / `update` / `publish-version` | `write:surveys` |
| `widgets list` | `read:widgets` |
| `widgets create` / `update` / `delete` | `write:widgets` |
| `usage` | `read:feedback` |

## Exit codes

| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General / unknown error |
| 2 | Usage / config error (bad flags, unreadable CSV, missing column) |
| 3 | Auth error (not logged in, invalid or expired token) |
| 4 | Forbidden or payment required (missing scope/role, plan without API access, quota exceeded, billing blocked) |
| 5 | Not found (unknown team/project/object or no access) |
| 6 | Validation error or invalid state transition |
| 7 | Network error (server unreachable) |

On API errors the API's error detail is printed to stderr.

## Environment variables

| Variable | Default | Purpose |
|---|---|---|
| `WITHFEEDBACK_API_URL` | `https://withfeedback.com` | API base URL (self-hosted/staging override) |
| `WITHFEEDBACK_TOKEN` | — | PAT (beats the keychain, loses to `--token`) |
| `WITHFEEDBACK_CLIENT_ID` | official public client id | Client id for `login` (override for self-hosted) |
| `WITHFEEDBACK_SCOPES` | all CLI scopes | Scopes requested during device-flow login |

On the first request of a run the CLI checks `GET /.well-known/speedpy.json`
once and warns (never fails) when the manifest is unreachable or the API is
older than the minimum this client expects.

## Development

```bash
uv run --with pytest --with httpx --with click --with keyring pytest packages/withfeedback-cli/
```
