> ## Documentation Index
> Fetch the complete documentation index at: https://www.pickfu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# PickFu CLI for surveys and automation

> Install the PickFu CLI on macOS, Linux, or Windows to create surveys, fetch results, and automate surveys with JSON output for CI/CD workflows.

The PickFu CLI (`@pickfu/cli`) lets you create surveys, check results, and manage surveys directly from your terminal. It supports structured JSON output for automation and CI/CD workflows.

## Installation

<CodeGroup>
  ```bash Homebrew theme={null}
  brew install pickfu/tap/pickfu
  ```

  ```bash macOS / Linux theme={null}
  curl -fsSL https://agents.pickfu.com/install.sh | bash
  ```

  ```powershell Windows theme={null}
  irm https://agents.pickfu.com/install.ps1 | iex
  ```

  ```bash npx (zero-install) theme={null}
  npx @pickfu/cli@latest survey list
  ```

  ```bash npm (global) theme={null}
  npm install -g @pickfu/cli
  ```
</CodeGroup>

### Supported platforms

| Platform | Architecture | Install method |
| - | - | - |
| macOS | Apple Silicon (arm64) | Homebrew, curl, npx, npm |
| macOS | Intel (x64) | Homebrew, curl, npx, npm |
| Linux | x64 | curl, npx, npm |
| Linux | arm64 | curl, npx, npm |
| Windows | x64 | PowerShell, npx, npm |

The curl/PowerShell installers download a standalone binary — no Node.js required. The `npx` method requires Node.js 20+ but needs no installation.

## Authentication

### OAuth (recommended)

Log in through the browser to connect the CLI to your PickFu account:

```bash theme={null}
pickfu auth login
```

This opens a browser window for OAuth authorization. Once approved, your credentials are stored locally and refreshed automatically. Pass `--profile <name>` to log in to a specific saved profile — useful when switching between staging and production. The CLI picks the staging or production WorkOS environment from the selected profile's `apiHost`. See [Profiles](#profiles).

For headless environments (servers, CI), use the `--headless` flag to print the authorization URL instead of opening a browser:

```bash theme={null}
pickfu auth login --headless
```

### API key

For non-interactive environments like CI pipelines or automation scripts, use an API key instead of OAuth:

```bash theme={null}
# Interactive prompt (key never appears in shell history)
pickfu auth set-key

# Or pipe from clipboard (macOS)
pbpaste | pickfu auth set-key

# Or use an environment variable (for CI/automation)
export PICKFU_API_KEY=sk_...
pickfu survey list

# Or pass per-command via flag
pickfu survey list --token sk_...
```

<Note>
  Avoid `echo "sk_..." | pickfu auth set-key` — the key appears in shell history. Use the interactive prompt or pipe from clipboard instead.
</Note>

**Token precedence:** `--token` flag > `PICKFU_API_KEY` env var > stored OAuth token > stored API key. When more than one profile is saved, `--profile <name>` selects which stored credential and which API host are used — see [Profiles](#profiles).

### Managing credentials

```bash theme={null}
pickfu auth status       # Check authentication status
pickfu auth status --local  # Check without contacting the API
pickfu auth logout       # Clear stored credentials
```

Credential-management commands such as `auth status`, `auth logout`, and `auth set-key` accept `--profile <name>` to act on a specific saved profile — for example `pickfu auth status --profile staging` or `pickfu auth logout --profile staging`. See [Profiles](#profiles) below.

### Profiles

A single CLI install can keep separate credentials and API hosts under named **profiles**. Use profiles to switch between environments (for example staging and production) or between different PickFu accounts without logging in and out each time.

Profiles live in `~/.config/pickfu/config.json` with file locking and `0600` permissions. Each profile stores its own OAuth tokens or API key alongside the `apiHost` those credentials belong to. The first profile you save automatically becomes the default; you can change the default later with `pickfu profile use`.

The first time a CLI on this release reads an older flat config, it upgrades the file in place to the new `{ profiles, default }` layout under the profile name `default`. No action is needed.

#### Save a profile

```bash theme={null}
# Save the currently-active credentials as a new profile
pickfu profile save staging

# Save against a specific API host
pickfu profile save staging --api-host https://api-staging.pickfu.com

# Overwrite an existing profile and make it the default
pickfu profile save production --force --set-default
```

Save reads whatever credential is currently active (`PICKFU_API_KEY`, an active OAuth session, or the credentials of the current default profile) and stores it under the new name. Profile names must match `^[a-zA-Z0-9._-]{1,64}$`. API hosts must be `https://` URLs, or `http://` on `localhost`, `127.0.0.1`, or `::1`.

<Note>
  `profile save` will not copy a stored OAuth session onto a different `apiHost` — those tokens are only valid for the host they were issued against. To seed a profile for a different environment (for example a sandbox host), export that environment's `PICKFU_API_KEY` before saving, or run `pickfu auth login --profile <name>` to log in against the new host first.
</Note>

| Flag | Description |
| - | - |
| `--api-host` | API host to bind to this profile |
| `--set-default` | Make the new profile the default |
| `--force` | Overwrite an existing profile without confirmation |

#### Switch the default profile

```bash theme={null}
pickfu profile use production
```

Subsequent commands (with no `--profile` flag and no `PICKFU_API_KEY` env var) use this profile's credentials and API host.

#### List and inspect profiles

```bash theme={null}
pickfu profile list             # Names, API hosts, auth type, default marker
pickfu profile list --json
pickfu profile show staging     # Full details for one profile
pickfu profile show staging --json
```

Both commands mask secrets: API keys display as `sk_****` with only the first and last few characters, access tokens display the first and last few characters with `****` in between when long enough, and refresh tokens are reported by length only. Neither command ever prints a full credential.

#### Delete a profile

```bash theme={null}
pickfu profile delete staging
pickfu profile delete staging --force              # Skip confirmation
pickfu profile delete only-profile --force-with-no-fallback  # Allow deleting the last profile
```

Deleting the current default profile clears the default; run `pickfu profile use <other>` to pick a new one. The CLI refuses to delete the last remaining profile unless you pass `--force-with-no-fallback`, so you don't accidentally end up with no stored credentials.

#### Select a profile per command

Every API command accepts a global `--profile <name>` flag — `auth logout`, `auth set-key`, `auth status`, `whoami`, `api`, and every `survey`, `media`, `project`, `playbook`, `targeting`, `reporting`, and `tag` subcommand. The flag chooses **both** the API host and the credential the request uses:

```bash theme={null}
# Run the same command against two environments
pickfu survey list --profile staging
pickfu survey list --profile production

# Update just the API key on the staging profile
pickfu auth set-key --profile staging
```

Without `--profile`, commands fall back to the default profile.

#### Token precedence with profiles

For a single request, the CLI picks a credential in this order:

1. `--token` flag
2. `PICKFU_API_KEY` env var
3. Stored credential on the selected profile — OAuth token first, then API key

`--profile` selects **which** stored credential (step 3) is considered. It also selects that profile's `apiHost`, unless `PICKFU_API_HOST` is set. `--token` and `PICKFU_API_KEY` override credentials only; they do not change the selected host.

#### `PICKFU_API_HOST` and stored credentials

`PICKFU_API_HOST` is intended for local development and staging tests. Setting it while a stored profile credential would otherwise be used is rejected:

```
PICKFU_API_HOST requires --token or PICKFU_API_KEY; refusing to send stored credentials.
```

Combine `PICKFU_API_HOST` only with `--token` or `PICKFU_API_KEY`, so you never accidentally send production credentials to an arbitrary host. To point a profile at a different host permanently, save a new profile with `--api-host`.

#### Agent introspection

`pickfu schema` includes an `agent_context` block so agent consumers can discover which profiles are available without reading the config file directly:

```json theme={null}
{
  "version": "...",
  "commands": [...],
  "agent_context": {
    "available_profiles": ["default", "staging"],
    "default_profile": "default",
    "skill_path": "/path/to/SKILL.md"
  }
}
```

`skill_path` is the absolute path of the `SKILL.md` usage guide that ships with the installed CLI.

## Timeouts and retries

Every CLI request has a deadline and gives up cleanly if the API takes too long to respond — so a stalled upstream will never hang your terminal or your CI job indefinitely.

### Request timeout

Each request aborts after **120 seconds** by default. Override it with the `PICKFU_TIMEOUT_MS` environment variable (in milliseconds):

```bash theme={null}
# Fail fast in CI health checks
PICKFU_TIMEOUT_MS=5000 pickfu survey list

# Allow more time for large media uploads or image generation
PICKFU_TIMEOUT_MS=300000 pickfu media generate --prompt "Product hero shot"
```

When a request times out, the CLI exits with a `Request timed out after Ns` error and a reminder to raise `PICKFU_TIMEOUT_MS` if needed.

### Automatic retries

The CLI automatically retries transient failures (network errors and 5xx responses) up to 3 times, but **only for idempotent methods** — `GET`, `HEAD`, `PUT`, and `DELETE`. `POST` requests are never auto-retried on network or 5xx errors, because a failed request may still have been processed server-side and retrying it could create duplicate surveys, duplicate publish charges, or duplicate tags.

If a command like `pickfu survey create`, `pickfu survey publish`, or `pickfu tag create` fails with a network error or 5xx, the CLI surfaces the error immediately. Check whether the operation actually succeeded (for example with `pickfu survey list` or `pickfu tag list`) before retrying by hand.

`429 Too Many Requests` responses are retried for every method — a 429 means the server rejected the request before processing it, so repeating is safe. The CLI honors the `Retry-After` header.

## Delivering output

The four output-producing commands accept a `--deliver` flag that routes the result somewhere other than stdout:

* `pickfu media generate`
* `pickfu media upload`
* `pickfu media show`
* `pickfu survey responses`

Use `--deliver` when you want the CLI itself to hand the artifact to the next step of your pipeline — for example, download a generated image straight to disk in a build script, or POST a survey's raw responses to your analytics service the moment they finish collecting. Every other command ignores the flag and rejects it if passed explicitly.

### Delivery modes

`--deliver` takes one of three forms:

| Value | What happens |
| - | - |
| `stdout` | Explicitly selects stdout delivery and prints the normal payload, same as omitting the flag. |
| `file:<path>` | Writes the payload to `<path>`. `~` expands to `$HOME`; relative paths resolve against the current directory. |
| `webhook:<url>` | POSTs the payload as JSON to `<url>`. |

Per-command semantics for `file:`:

| Command | What `--deliver=file:<path>` writes |
| - | - |
| `media generate` | Downloads the generated media from its CDN URL into `<path>` (bytes, not JSON). |
| `media show` | Writes the metadata JSON to `<path>`. |
| `survey responses` | Writes the JSON dump to `<path>`. |
| `media upload` | Refused. The uploaded asset is already on the CDN — use its returned URL directly. |

### File delivery

File writes are atomic: the CLI writes to a sibling `.tmp.*` file and renames into place, so a reader either sees the previous file or the fully-written new one, never a half-written payload. Delivered files are created with mode `0600` — private to you — so delivery never broadens permissions on sensitive output like survey responses.

Pass `--no-clobber` to refuse to overwrite an existing file. The check is race-free (the CLI publishes via `link()`), so two concurrent writers cannot both succeed against the same path.

```bash theme={null}
# Download a generated image straight to disk
pickfu media generate --prompt "a red sneaker" --deliver=file:./sneaker.png

# Snapshot survey responses to a JSON file, refusing to overwrite
pickfu survey responses svy_abc123 --deliver=file:~/exports/svy_abc123.json --no-clobber

# Save media metadata as JSON
pickfu media show https://media-cdn.pickfu.com/.../v1.jpg --deliver=file:./meta.json
```

### Webhook delivery

`--deliver=webhook:<url>` performs a single-shot, unsigned `POST` of the exact JSON payload that would have gone to stdout, with:

* `Content-Type: application/json`
* `User-Agent: pickfu-cli/<version>`

Behavior details:

* HTTPS URLs are recommended. Plain `http:` warns; other schemes are refused.
* URLs with embedded credentials (`https://user:pass@host/...`) are refused.
* Redirects are **not** followed — a `3xx` is reported as a delivery failure (`WEBHOOK_FAILED`), not silently retried as a body-less `GET`.
* Any non-2xx response exits nonzero. There is no automatic retry.
* The request honors `PICKFU_TIMEOUT_MS`.

```bash theme={null}
pickfu media generate --prompt "Product hero shot" \
  --deliver=webhook:https://hooks.example.com/pickfu
```

### Stdout with `--deliver`

When `--deliver` is set to `file:` or `webhook:`, the CLI replaces the stdout payload with a **delivery confirmation** object. Explicit `--deliver=stdout` leaves the normal payload on stdout.

```json theme={null}
{
  "delivered_to": "file:/Users/me/sneaker.png",
  "bytes": 184320,
  "source_url": "https://media-cdn.pickfu.com/.../abcd.png",
  "content_type": "image/png"
}
```

| Field | When it's set |
| - | - |
| `delivered_to` | Always. For webhooks, redacted to the receiver's origin (e.g. `webhook:https://hooks.example.com`) since path and query are often secret. |
| `bytes` | File delivery. |
| `status` | Webhook delivery. The receiver's HTTP status code. |
| `source_url` | `media generate`, `media upload`, or `media show` — the CDN URL of the artifact. |
| `content_type` | `media generate` with file delivery — echoes the downloaded CDN `Content-Type`. |

<Warning>
  **Breaking change for scripts that read `.url` from stdout.** With `--deliver=file:` or `--deliver=webhook:`, the stdout JSON is a confirmation and no longer contains the result payload's `url` field. If you have a pipeline like `pickfu media generate --json | jq -r .url`, switch to `jq -r .source_url` when you add delivery, or use `--deliver=stdout` (or omit `--deliver`) to keep the payload on stdout.
</Warning>

### Introspecting delivery support

`pickfu schema` reports `deliveryModes` and `deliveryDefault` for each deliverable command, so integrations can discover which modes apply without hard-coding the list:

```bash theme={null}
pickfu schema media generate
```

## Validation errors

When the API rejects input with a `422` that includes violations, for example from template limits, the CLI prints the server's message followed by each violation's `param`, allowed values, and received value:

```text theme={null}
Error: Template "Business name test" offers sample sizes 15, 30; you sent 75. ... (422)
  sampleSize: Template "Business name test" offers sample sizes 15, 30; you sent 75.
    allowed: 15, 30
    received: 75
  country: Template "Business name test" offers countries US; you sent GB.
    allowed: US
    received: GB
```

With `--json`, the error object adds a `violations` array next to `status`, `message`, `code`, and `suggestions`:

```json theme={null}
{
  "status": 422,
  "message": "Template \"Business name test\" offers sample sizes 15, 30; you sent 75. ...",
  "code": "VALIDATION_ERROR",
  "suggestions": ["Fix the errors below and try again"],
  "violations": [
    {
      "code": "COMBO_RESPONSE_COUNT",
      "param": "sampleSize",
      "message": "Template \"Business name test\" offers sample sizes 15, 30; you sent 75.",
      "allowed": ["15", "30"],
      "received": "75"
    }
  ]
}
```

Scripts and agents can fix each `param` using its `allowed` value. `allowed` can be a list of values, a `{ "min", "max" }` range, or a targeting rule. A `422` without violations still prints `Validation failed` with any field errors.

## Commands

All commands support `--json` for machine-readable output.

### Surveys

#### Create a survey

```bash theme={null}
pickfu survey create
```

The CLI walks you through survey creation interactively. You can also provide options via flags:

```bash theme={null}
pickfu survey create \
  --name "Logo test" \
  --question "Which logo do you prefer?" \
  --type head_to_head \
  --option "Logo A" \
  --option "Logo B" \
  --sample-size 50 \
  --country US \
  --targeting amznpr \
  --reporting age \
  --tag "Q2 testing"
```

Image surveys use `--option-image-url` instead of `--option`:

```bash theme={null}
pickfu survey create \
  --name "Packaging variants" \
  --question "Which design works best?" \
  --type ranked \
  --option-image-url https://cdn.example.com/v1.jpg \
  --option-image-url https://cdn.example.com/v2.jpg \
  --option-image-url https://cdn.example.com/v3.jpg \
  --sample-size 50 \
  --project proj_xyz
```

Or create from a JSON file:

```bash theme={null}
pickfu survey create --from-file survey.json
```

<Expandable title="JSON file format">
  ```json theme={null}
  {
    "name": "Logo test",
    "country": "US",
    "sampleSize": "50",
    "externalId": "exp-2026-091",
    "surveyIntent": "Determine which logo resonates better with our target audience",
    "questions": [
      {
        "type": "head_to_head",
        "prompt": "Which logo do you prefer for a children's educational app?",
        "context": {
          "text": "We're launching a new learning app for kids aged 5-10"
        },
        "options": [
          { "value": "Logo A — playful, colorful design" },
          { "value": "Logo B — clean, modern design" }
        ]
      }
    ],
    "targeting": ["amznpr"],
    "reporting": ["age", "gender"],
    "tags": [{ "name": "Q2 testing" }],
    "projectId": "proj_xyz"
  }
  ```

  Image options use `mediaUrl` (not `imageUrl`); product options use `asin`; image-set options use `imageSet`. Each option must set exactly ONE of `value`, `mediaUrl`, `asin`, `imageSet`, or `mockup` — never combine them on one option, and do NOT add a text `value` to an image option (image options are auto-labeled A, B, C). All options within the same question must share the same content type. `mediaUrl` values are classified by content: image URLs become image options, YouTube, Vimeo, and Wistia links become embedded video options, and any other URL becomes a generic link option — so a single question cannot mix video URLs with other kinds of URLs. Unknown fields on options are rejected with a 400 — see the [API reference](/docs/api-reference/surveys/create-survey) for the full schema.
</Expandable>

| Flag | Description |
| - | - |
| `--from-file` | Path to a JSON file with survey definition |
| `--name` | Survey name |
| `--question` | The question text |
| `--type` | Question type (see [supported types](#supported-question-types)) |
| `--option` | Text option (repeatable — use once per option) |
| `--option-image-url` | Image option URL (repeatable, mutually exclusive with `--option`) |
| `--sample-size` | Number of respondents |
| `--country` | Two-letter country code. Omit to default to `US`. |
| `--targeting` | Targeting trait code, e.g. `amznpr` (repeatable) |
| `--reporting` | Reporting trait code, e.g. `age` (repeatable) |
| `--project` | Project ID to assign the survey to |
| `--tag` | Tag name to attach (repeatable) |

#### Correlate surveys with your own system

Add an optional `externalId` to the `--from-file` JSON to link a survey to a record in your own system, such as an experiment ID, a ticket number, or a database key. PickFu stores the value as-is and returns it on `survey get` and `survey list` output. It is omitted when unset.

```json theme={null}
{
  "name": "Logo test",
  "externalId": "exp-2026-091",
  "questions": [{ "type": "head_to_head", "prompt": "...", "options": [] }]
}
```

Look surveys up later by exact match with the `externalId` query parameter:

```bash theme={null}
pickfu api GET /v1/surveys --query externalId=exp-2026-091
```

Rules:

* 1-255 characters after trimming whitespace. Control characters are rejected.
* Matching is exact, case-sensitive, and byte-exact. It is not a text search, so send the identical string every time.
* The key is scoped to your account and doesn't have to be unique. Several surveys can share one `externalId`, for example every survey in one experiment.
* **Set-once.** You can also claim an `externalId` on an existing draft with `pickfu survey update <survey-id> --from-file`. Once set it cannot be changed, and it cannot be added after the survey is published. Either attempt returns `409 Conflict`. Sending the same value again succeeds (idempotent).
* `pickfu survey clone` does not copy `externalId` to the new draft.
* Mini-polls cannot have an `externalId` (rejected with `400`).

#### Create a survey from a template

Templates are ready-made single-question surveys with a fixed price per respondent. To start a draft from one, put a `template` block in the `--from-file` JSON in place of `questions`. Send exactly one of `questions` or `template`.

Find a template id (`cmb_<slug>`) and read its blanks and option slots first:

```bash theme={null}
pickfu api GET /v1/templates
pickfu api GET /v1/templates/<template-id>
```

Then create the draft. The template id and blank name below are examples; use the values from your template's detail response.

```json survey.json theme={null}
{
  "template": {
    "id": "cmb_business-name-test",
    "blanks": { "madlib_business_description": "a neighborhood coffee roastery" },
    "options": [{ "value": "Ember & Bean" }, { "value": "Corner Roast Co." }, { "value": "Daybreak Roasters" }]
  }
}
```

```bash theme={null}
pickfu survey create --from-file survey.json
```

The `template` block accepts `id` (required), `prompt`, `blanks`, `context`, and `options`:

* `blanks` fills each `{{name}}` in the template's question. Every blank is required.
* `prompt` replaces the question on templates without blanks. It's rejected on templates that have blanks.
* The template sets the question type. Where a template offers both, two options or fewer run head-to-head and three or more are ranked.
* When you omit `sampleSize`, `country`, `name`, or `targeting`, the draft uses the template's defaults.
* You can't send `reporting`. A template's reporting traits are fixed.
* The block is create-only. Edit the draft afterwards with `pickfu survey update` and `questions`.

A draft outside the template's limits is rejected with a `422` that lists every violation (see [Validation errors](#validation-errors)). Run `pickfu schema survey create` for the full input schema. See [templates in the MCP server](/docs/integrations/mcp-server#templates) and the [Create survey API reference](/docs/api-reference/surveys/create-survey) for details.

#### Get survey details

```bash theme={null}
pickfu survey get <survey-id>
```

#### List surveys

```bash theme={null}
pickfu survey list
```

| Flag | Description | Default |
| - | - | - |
| `--status` | Filter by status (`completed`, `in-progress`, `unpublished`, `archived`) | All |
| `--limit` | Results per page | 20 |
| `--page` | Page number | 1 |
| `--sort-by` | Sort field (`createdAt`, `name`, `creator`, `status`) | `createdAt` |
| `--dir` | Sort direction (`asc`, `desc`) | `desc` |

#### Update a draft survey

```bash theme={null}
pickfu survey update <survey-id> --name "Updated name" --sample-size 100
```

Or update from a JSON file:

```bash theme={null}
pickfu survey update <survey-id> --from-file updated-survey.json
```

| Flag | Description |
| - | - |
| `--from-file` | Path to JSON file with updates |
| `--name` | Updated survey name |
| `--sample-size` | Updated respondent count |
| `--country` | Updated country code |

The `--from-file` JSON can also include an `externalId` to claim a correlation key on a draft that doesn't have one yet. See [Correlate surveys with your own system](#correlate-surveys-with-your-own-system) for the set-once rules.

#### Publish a survey

Publish a draft survey to start collecting responses:

```bash theme={null}
pickfu survey publish <survey-id>
```

<Note>
  Calling `publish` on an already-published survey returns a `400 Already published` with a pointer to the [`/v1/surveys/{id}/respondents`](/docs/api-reference/surveys/add-respondents) endpoint. Use `pickfu survey add-respondents` to extend the sample size of a running survey instead.
</Note>

<Note>
  Publishing spends the account's credit balance first. When credits don't cover the full cost, the saved card on file is charged for exactly the shortfall (order total minus credits and any coupon or discount already applied) and the survey launches. No credits are purchased on the caller's behalf.

  Publish returns `402 Payment Required` only when the account has no saved card, the card was declined, or the card requires 3D Secure authentication (which cannot be completed over the API). Add or update a card on your PickFu billing page, then retry.
</Note>

#### Stop a survey

Stop a live survey from collecting responses before it reaches its target sample size:

```bash theme={null}
pickfu survey stop <survey-id>
pickfu survey stop <survey-id> --json
```

#### Clone a survey

Duplicate an existing survey as a new draft. Useful for re-running a survey with the same audience targeting and options, or for iterating on a previous test:

```bash theme={null}
pickfu survey clone <survey-id>
pickfu survey clone <survey-id> --name "Round 2" --sample-size 30
pickfu survey clone <survey-id> --project proj_xyz --json
```

| Flag | Description |
| - | - |
| `--name` | Override the cloned survey name (defaults to "Copy of \<name>") |
| `--sample-size` | Override sample size (defaults to source survey) |
| `--project` | Project ID to assign the clone to |

The clone does not inherit the source survey's `externalId`. Set a fresh one on the new draft if you track re-runs separately in your own system.

#### Extract image URLs

List the image URLs of every option in a survey — useful for downloading source images for further processing:

```bash theme={null}
pickfu survey extract-images <survey-id>
pickfu survey extract-images <survey-id> --json
```

#### View responses

Get individual responses with explanations and demographics:

```bash theme={null}
pickfu survey responses <survey-id>
```

| Flag | Description | Default |
| - | - | - |
| `--page` | Page number | 1 |
| `--limit` | Responses per page | 20 |
| `--question-id` | Filter responses by question | All questions |
| `--deliver` | Route the JSON dump to `stdout`, `file:<path>`, or `webhook:<url>`. See [Delivering output](#delivering-output). | `stdout` |
| `--no-clobber` | With `--deliver=file:`, refuse to overwrite an existing file. | false |

#### Watch a survey

Block until a survey completes — useful in scripts and CI:

```bash theme={null}
pickfu survey watch <survey-id>
```

| Flag | Description | Default |
| - | - | - |
| `--interval` | Polling interval in seconds | 30 |
| `--exit-status` | Exit with code 2 if cancelled or archived | false |
| `--json` | Emit one NDJSON object per poll tick to stdout, plus a final completion object | false |

#### Delete a survey

Delete a draft survey, or archive one that has already been published:

```bash theme={null}
pickfu survey delete <survey-id>
pickfu survey delete <survey-id> --json
```

<Note>
  For a draft survey this permanently deletes it. For a published survey it archives instead — the survey still appears in `pickfu survey list --status archived`.
</Note>

### Targeting and reporting

#### List available targeting traits

```bash theme={null}
pickfu targeting list --country US
```

Returns all demographic targeting traits available for the specified country. Pass each `permalink` value as a `--targeting` flag in `survey create` (e.g. `--targeting amznpr`).

Targeting a non-US country also localizes the survey: PickFu translates the question, context, and options into that market's language and returns results in both languages. See [International surveys and translation](/docs/guides/international-surveys).

#### List available reporting demographics

```bash theme={null}
pickfu reporting list --country US
```

Returns all demographic breakdowns available for survey results.

### Tags

```bash theme={null}
pickfu tag list                    # List your tags
pickfu tag list --query "amazon"   # Search tags
pickfu tag create "Q2 testing"     # Create a new tag
```

### Media

#### Generate an image

Generate an image from a text prompt and get a CDN URL:

```bash theme={null}
pickfu media generate --prompt "A playful logo for a kids app"
pickfu media generate --prompt "Product banner" --aspect-ratio 16:9
```

Pass one or more reference images so the model can preserve product appearance across regenerated variants — useful for iterating on packaging, A+ content, or main-image variants:

```bash theme={null}
pickfu media generate \
  --prompt "Same product, holiday-themed packaging" \
  --reference-image-url https://cdn.example.com/source.jpg \
  --aspect-ratio 1:1
```

| Flag | Description | Default |
| - | - | - |
| `--prompt` | Text prompt describing the image (required) | |
| `--aspect-ratio` | Aspect ratio (`1:1`, `16:9`, `9:16`, `3:2`, `2:3`, etc.) | `1:1` |
| `--reference-image-url` | Reference image URL the model can see while generating (repeatable, max 3) | |
| `--deliver` | Route the result to `stdout`, `file:<path>`, or `webhook:<url>`. See [Delivering output](#delivering-output). | `stdout` |
| `--no-clobber` | With `--deliver=file:`, refuse to overwrite an existing file. | false |

<Note>
  **Reference image hosts.** `--reference-image-url` and `pickfu survey extract-images` fetch bytes from the remote host, so the URL must be on PickFu's allowlist: `pickfu.com` (including `media-cdn.pickfu.com`), `cloudinary.com`, S3 (`s3.amazonaws.com` and regional buckets), CloudFront (`d*.cloudfront.net`), Amazon product CDNs (`m.media-amazon.com`, `ssl-images-amazon.com`, `images-amazon.com`), and Alibaba / Aliyun CDNs (`alicdn.com`, `aliyuncs.com`). If your image is elsewhere, upload it first with `pickfu media upload` and use the returned CDN URL.
</Note>

#### Upload media

Upload an image or video to the PickFu CDN from a URL or local file:

```bash theme={null}
pickfu media upload --url https://example.com/image.png
pickfu media upload --file ./logo.png
pickfu media upload --file ./demo.mp4 --resource-type video
```

| Flag | Description |
| - | - |
| `--url` | URL of the media to upload (mutually exclusive with `--file`) |
| `--file` | Path to a local file to upload (mutually exclusive with `--url`) |
| `--filename` | Display name hint for the uploaded media |
| `--resource-type` | `image` or `video` (auto-detected if omitted) |
| `--deliver` | Route the confirmation to `stdout` or `webhook:<url>`. `file:` is refused because the uploaded asset is already on the CDN. See [Delivering output](#delivering-output). |

YouTube, Vimeo, and Wistia URLs are returned as-is since PickFu embeds them natively.

#### Inspect media

Fetch metadata (content type, size, ETag) for an image or video URL without downloading the full payload:

```bash theme={null}
pickfu media show https://media-cdn.pickfu.com/.../v1.jpg
pickfu media show https://media-cdn.pickfu.com/.../v1.jpg --open
pickfu media show https://media-cdn.pickfu.com/.../v1.jpg --json
```

| Flag | Description | Default |
| - | - | - |
| `--open` | Open the URL in the system default browser after fetching metadata | false |
| `--deliver` | Route the metadata JSON to `stdout`, `file:<path>`, or `webhook:<url>`. See [Delivering output](#delivering-output). | `stdout` |
| `--no-clobber` | With `--deliver=file:`, refuse to overwrite an existing file. | false |

### Projects

```bash theme={null}
pickfu project list                          # List projects
pickfu project create --name "Q2 Research"   # Create a project
pickfu project get <project-id>              # Get project details
pickfu project update <project-id> --name "Updated name"
```

| Flag | Description |
| - | - |
| `--name` | Project name |
| `--description` | Project description |
| `--bookmark` | Bookmark the project |
| `--archive` | Archive the project |

### Playbooks

Browse step-by-step research guides:

```bash theme={null}
pickfu playbook list                     # List available playbooks
pickfu playbook get <slug>               # Get full playbook with instructions
```

### Schema introspection

View the full input/output schemas for any command — useful for building integrations:

```bash theme={null}
pickfu schema             # List all commands
pickfu schema survey.create  # Detailed schema for a specific command
```

### Generic API access

Make authenticated API requests directly:

```bash theme={null}
pickfu api GET /v1/surveys
pickfu api POST /v1/surveys --body '{"name": "test"}'
pickfu api GET /v1/traits/targeting --query country=US
```

### Identity

Print which credential the CLI is using, decode its JWT claims, and verify it against the API:

```bash theme={null}
pickfu whoami          # Local credential + live API verification
pickfu whoami --local  # Skip the API call
pickfu whoami --json   # Machine-readable output
```

`whoami` reports the credential source (`--token`, `PICKFU_API_KEY`, stored OAuth session, or stored API key), the profile name, the API host, and whether verification succeeded. For OAuth credentials, it also reports the account email, user id, granted scopes, and expiry decoded from the JWT; API-key authentication has no JWT identity claims, so those fields are null. Without `--local`, it calls `GET /v1/tags?limit=1` to confirm the credential is accepted and captures the rate-limit headers.

Exit codes are the contract for scripts and agents: `0` when the API host shown accepted the identity, `1` when the credential is missing, rejected, expired, or the host was unreachable.

```
Account:    you@example.com (user 42)
Auth:       oauth, stored session, expires in 6h
Scopes:     surveys:read surveys:write media:write
Profile:    default
API host:   https://api.pickfu.com
Rate limit: 998/1000 remaining, resets in 42s
Verified:   yes
```

<Note>
  The verification request can proactively refresh a stale OAuth session and rotate the stored access token. `whoami` reports the post-refresh identity, so the email, scopes, and expiry always describe the credential that was used to reach the API.
</Note>

### Local webhook development

Develop webhook consumers locally before you point PickFu at your production endpoint. `pickfu listen` runs a loopback HTTP forwarder that signs test events, and `pickfu trigger` fires those events on demand.

`listen` and `trigger` are a **local fixture harness**, not a production-webhook replica. They share only the signature scheme (`t=<unix>,v1=<hmac-sha256>` over `<t>.<body>`) with the production v2 webhooks PickFu delivers. The envelope fields, the extra headers, and the event types below all differ from production, which sends a narrower catalog (`survey.completed`, `order.paid`). For the real delivery shape, see the live [OpenAPI contract](https://api.pickfu.com/v2/openapi.json).

#### Start a listener

```bash theme={null}
# Terminal 1 — forward signed events to your local endpoint
pickfu listen --forward-to localhost:4242/webhooks
```

The listener binds to `127.0.0.1` on port `11800` (falling through to `11801` and `11802` when busy). Use `--port <n>` to pin a port, or `--port 0` to pick an ephemeral one. On startup it prints the signing secret (`whsec_…`) your consumer needs, records a session file so `pickfu trigger` can find it, and stays running until you press `^C` (exit code `130`).

| Flag | Description | Default |
| - | - | - |
| `--forward-to` | Local endpoint to forward signed events to (required, e.g. `localhost:4242/webhooks`) | |
| `--port` | Loopback port to bind; `0` picks an ephemeral port | `11800` (falls through `11800`–`11802`) |
| `--events` | Comma-separated event types to forward. Everything else is acknowledged and skipped. | All events |
| `--secret` | Reuse a specific `whsec_…` secret across restarts | Reuse the last session's secret, or generate a new one |
| `--timeout` | Seconds to wait for your endpoint to respond | `30` |
| `--json` | Emit one NDJSON record per delivery to stdout (a `ready` record on startup, then a `delivery` record per event) | false |

Only `http://` and `https://` targets are allowed, and URLs with embedded credentials (`user:pass@…`) are rejected — the CLI never forwards credentials inside a signed payload. Trigger request bodies are capped at 1 MiB (`413 Payload Too Large` above that).

#### Trigger a test event

```bash theme={null}
# Terminal 2 — fire a test event through the running listener
pickfu trigger survey.completed
pickfu trigger survey.completed --survey-id srv_abc123 --fetch
pickfu trigger media.generated --data '{"url":"https://example.com/a.png","kind":"image"}'

# Or deliver directly, without a listener
pickfu trigger survey.created \
  --forward-to localhost:4242/webhooks \
  --secret whsec_...
```

`pickfu trigger <event>` builds a signed envelope and, by default, posts it to `POST /trigger` on the running `pickfu listen` session so your endpoint sees the local listener's signing scheme and delivery shape. These are local fixture events; they are not the production webhook catalog. Pass `--forward-to` to bypass the listener and post directly (`--secret` is required unless a previous listen session's secret is on disk).

The event catalog:

| Event | Fixture event |
| - | - |
| `survey.created` | A draft survey is created |
| `survey.published` | A survey is published and starts collecting responses |
| `survey.responded` | A single respondent completes a survey |
| `survey.completed` | A survey reaches its target sample size |
| `survey.cancelled` | A running survey is stopped or archived |
| `media.generated` | An image finishes generating |
| `media.uploaded` | An asset finishes uploading to the CDN |

By default, `trigger` uses a synthetic stub for the event's `data.object`. Survey stubs include `status`, `numResponses`, and `id`; media stubs include `id`, `url`, and `kind`. All stubs include a `_pickfu_fixture: true` marker so you can tell test payloads from real deliveries. To send real data instead:

| Flag | Description |
| - | - |
| `--survey-id` / `--media-id` | Set the `id` on the stub object |
| `--fetch` | Fetch the survey (`GET /v1/surveys/<id>`) and use it as the event object. Requires `--survey-id`. |
| `--data` | Inline JSON object used verbatim as the event object. `@path/to/file.json` reads from a file. Mutually exclusive with `--fetch`. |
| `--forward-to` | Skip the running listener and deliver directly to this URL |
| `--secret` | Signing secret for `--forward-to`. Defaults to the last listen session's secret. |

`trigger` exits `0` when the endpoint returns `2xx` and `1` when the delivery times out, errors, or the endpoint returns a non-`2xx` response.

#### Wire format and signature verification

Every fixture delivery from `pickfu listen` is a `POST` to your `--forward-to` URL with `Content-Type: application/json` and an envelope shaped like:

```json theme={null}
{
  "id": "evt_9f1a...",
  "type": "survey.completed",
  "api_version": "v1",
  "created": 1757520000,
  "data": { "object": { "id": "srv_abc123", "status": "completed", "...": "..." } },
  "request": null
}
```

Headers on every fixture delivery (production sends only `Content-Type` and `X-PickFu-Signature`):

| Header | Value |
| - | - |
| `X-PickFu-Signature` | `t=<unix-seconds>,v1=<hmac-sha256-hex>` |
| `X-PickFu-Event-Id` | The event's `id` (matches `evt_…` in the body) |
| `X-PickFu-Event-Type` | Same as `type` in the body |
| `X-PickFu-Delivery-Attempt` | Delivery attempt number, starting at `1` |
| `User-Agent` | `PickFu/v1 (https://www.pickfu.com)` |

To verify a delivery, compute `HMAC-SHA256(secret, "<t>.<raw request body>")` and compare it to the `v1=` value in `X-PickFu-Signature` with a timing-safe compare. Reject the request if `t` is more than 300 seconds away from the current time. This matches the Stripe-style scheme, so an existing Stripe verifier works with only the header name and secret prefix swapped.

```js theme={null}
import { createHmac, timingSafeEqual } from 'node:crypto';

function verify(rawBody, header, secret) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;

  const expected = createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');
  const expectedBuffer = Buffer.from(expected, 'hex');
  const signatureBuffer = Buffer.from(parts.v1 ?? '', 'hex');
  return signatureBuffer.length === expectedBuffer.length
    && timingSafeEqual(expectedBuffer, signatureBuffer);
}
```

<Note>
  `pickfu listen` has no retries, replay, or event history. If your endpoint returns a non-`2xx` the delivery is recorded as failed and dropped. Fire the event again with `pickfu trigger` to retry.
</Note>

### Fixtures

Run a fixture file — a named sequence of API calls that sets up demo data with references between steps. Useful for seeding a fresh account for a demo, reproducing an integration bug on a clean environment, or standing up a scenario in a test suite.

```bash theme={null}
pickfu fixtures fixtures/basic-survey.json               # Run the fixture
pickfu fixtures fixtures/basic-survey.json --dry-run     # Print the plan, no requests
pickfu fixtures fixtures/basic-survey.json --var suffix=demo-42
cat fixtures/basic-survey.json | pickfu fixtures -       # Read from stdin
```

| Flag | Description | Default |
| - | - | - |
| `--var` | Set or override a fixture variable: `--var key=value` (repeatable) | |
| `--dry-run` | Resolve variables and print the plan without sending any requests | false |
| `--allow-charges` | Allow steps that spend account balance (publish, respondents, media generation, mini-polls, synthetic feedback) | false |
| `--json` | Emit the machine-readable run result | false |

#### File format

A fixture is a JSON object with `version`, `name`, optional `vars` defaults, and an ordered `steps` array. Each step is one API request.

```json theme={null}
{
  "version": 1,
  "name": "basic-survey",
  "vars": { "suffix": "dev" },
  "steps": [
    {
      "name": "project",
      "method": "POST",
      "path": "/v1/projects",
      "body": { "name": "Fixture project ${var:suffix}" }
    },
    {
      "name": "survey",
      "method": "POST",
      "path": "/v1/surveys",
      "body": {
        "name": "Fixture survey ${var:suffix}",
        "projectId": "${project:id}",
        "sampleSize": "15",
        "country": "US",
        "questions": [
          {
            "type": "head_to_head",
            "prompt": "Which headline is clearer?",
            "options": [
              { "value": "Ship faster" },
              { "value": "Ship with confidence" }
            ]
          }
        ]
      }
    },
    {
      "name": "tag",
      "method": "POST",
      "path": "/v1/tags",
      "body": { "name": "fixture-${var:suffix}" }
    },
    {
      "name": "tagged",
      "method": "PATCH",
      "path": "/v1/surveys/${survey:id}",
      "body": { "tags": [{ "id": "${tag:id}" }] }
    }
  ]
}
```

References are string-substituted anywhere in a step's `path`, `query`, or `body`:

* `${var:key}` reads a `--var` override, falling back to the `vars` default in the file.
* `${stepName:dotted.path}` reads a field from an earlier step's response, indexed by that step's `name`. The `data` envelope is unwrapped automatically, and array indices work (`${survey:questions.0.id}`).

Steps run sequentially. On the first failure the run stops and reports which steps completed — **earlier steps are not rolled back**, so you may need to delete any drafts or tags the fixture created before rerunning.

#### Guarding money-moving steps

By default `pickfu fixtures` refuses to send requests that can spend account balance. The gated paths are:

* `POST /v1/surveys/:id/publish`
* `POST /v1/surveys/:id/respondents`
* `POST /v1/media/generate`
* Any `/mini-polls` endpoint
* `POST /v1/synthetic/feedback`

Pass `--allow-charges` to opt in. The check runs against the fully resolved path immediately before dispatch, so a `${var:…}` or `${step:…}` reference cannot smuggle a charging endpoint through. Host-relative overrides (a `path` that starts with `//`) are rejected outright.

## OpenClaw

See the [OpenClaw page](/docs/integrations/openclaw) to run the PickFu CLI inside an agentic skill, including the ready-made Claw Hub market research workflow.

## Supported question types

<Note>
  **Every response includes a written explanation by default.** When a respondent picks an
  option, ranks, rates, or clicks, they're also required to explain *why* in their own words
  — that's already part of every question type except `screen_recording` (which returns a
  transcription instead). **Do not create a multi-question survey (`--question` twice) just
  to add a separate "explain why" question — it wastes a question slot (priced per question)
  and produces redundant data.** Phrase your real question with `--question`, and PickFu
  collects the explanation automatically. The text comes back on each response as the
  `explanation` field. See [surveys overview](/docs/guides/surveys-overview) for more.
</Note>

| Type | Description |
| - | - |
| `head_to_head` | A/B comparison where respondents pick a favorite (2 options) |
| `ranked` | Respondents rank options in preference order (3-8 options) |
| `open_ended` | Single-concept survey with written feedback |
| `click_test` | Heatmap generation from image clicks |
| `emoji_rating` | Emoji sentiment selection with explanation |
| `star_rating` | 1-5 star rating with feedback |
| `five_second_test` | Brief timed exposure for first impressions |
| `screen_recording` | Record user interaction with content |
| `single_select` | Choose one from multiple options (3-8 options) |
| `multi_select` | Select multiple answers from options (3-8 options) |

## Sample sizes

Available respondent counts: **15**, **30**, **50**, **75**, **100**, **200**, **300**, **500**

<Note>
  **Screen recording questions use a different set:** **5**, **10**, **15**, **20**, **25**, **30**, **50**, **75**. Passing any other value (for example `--sample-size 100`) on a `screen_recording` survey fails validation.
</Note>

## Need help?

```bash theme={null}
pickfu --help
pickfu survey --help
pickfu survey create --help
```

Or contact [support@pickfu.com](mailto:support@pickfu.com) for assistance.


## Related topics

- [How do I create my first survey?](/help/how-do-i-create-my-first-poll.md)
- [Authenticate the PickFu CLI, MCP server, and API](/docs/authentication.md)
- [How to build PickFu surveys with URL parameters](/help/how-to-build-pickfu-surveys-with-url-parameters.md)
- [PickFu survey URL parameters reference](/docs/guides/url-parameters.md)
- [Organize PickFu surveys with projects and tags](/docs/guides/projects-and-tags.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.