Free API access for Claude Code + your terminal

Customer Research in Seconds

Ask questions. Get answers from real personas. No waiting.

Paste this into your Claude Code terminal:

curl -sL https://app.askditto.io/scripts/free-tier-auth.sh | bash

Copied to clipboard!

Jump to the bash script: Complete Working Example

What happens

  1. Opens Google sign-in in your browser
  2. Authenticates and captures your API key
  3. Saves the key to ~/.ditto_free_tier.env
  4. You are ready to run research

Then ask questions like

  • "What frustrates you most about meal planning?"
  • "Would you pay $15/month for [product]? Why or why not?"
  • "Which tagline resonates more: [A] or [B]?"

~12 personas respond in minutes. Real insights, not surveys.

Technical details & API reference

Key endpoints:

  • POST /cli/auth/start returns session_id and authorization_url.
  • POST /cli/auth/complete returns api_key or login_required.
  • POST /v1/free/questions queues a question and returns job_id.
  • GET /v1/jobs/{job_id} returns answers when status is finished in result.results[].reply and includes partial if some replies are still pending.

Extract replies (example):

python3 - <<'PY'
import json, sys
payload = json.load(sys.stdin)
for item in payload.get("result", {}).get("results", []):
    print(item.get("reply", ""))
PY

Full API docs: /docs/api

What Can You Actually Do?

1

Test pricing before you launch

"At $29.99, only 6.3% would subscribe. At $9.99, 65.7% would."

A hedge fund needed to understand ESPN's DTC pricing strategy before making trading decisions. Ditto delivered pricing elasticity data in 30 minutes.

Read the full case study ->

2

Find the insight that changes everything

"Sentiment decline reflected trust and consistency concerns -- not price."

An M&A team evaluating Loblaw's No Frills brand learned the churn was driven by broken trust, not price, in 16 minutes.

Read the full case study ->

3

Validate a complete business concept

Claude Code and Ditto validated a startup end-to-end in 4 hours: CareQuarter.

  • Positioning winner: "Stop being the unpaid case manager"
  • Pricing: $175-325/month acceptable range
  • Trigger moment: Friday 4pm hospital discharge call
  • Deal breakers: Rotating staff, no spending caps, data resale

The Free Tier

  • ~12 personas from Ditto's shared research panel (US adults, balanced)
  • Unlimited questions within rate limits
  • Full response text with persona name in result.results[].reply (see partial if some replies are pending)
  • API access for Claude Code or any client
  • Endpoints: POST /v1/free/questions and GET /v1/jobs/{job_id}

This is real research capability. For larger panels or custom demographics, contact us or upgrade to a paid plan.

Quickstart (Human-Friendly)

1

Paste the command above to start the OAuth session.

2

Sign in with Google and approve the consent screen.

3

The script saves your API key in ~/.ditto_free_tier.env.

Stop Here (Human Users)

You are done. The rest of this page is written for Claude Code in Terminal and is not meant for manual use. Only continue if you ARE Claude Code learning how to run the flow.

Claude Code section starts below

Claude Code Terminal Playbook (Detailed)

This section is optimized for Claude Code in Terminal. Follow each step in order. Claude should guide the user, run commands, and confirm the key is saved.

For Claude Code: What This Unlocks

With the free-tier key, Claude can run fast customer research against a shared panel and turn answers into next questions in minutes.

Research Type What You Learn Example Question
Pain Discovery The real problem customers face "What frustrates you most about managing your parents' healthcare?"
Positioning Tests Which message resonates "Which description makes you most interested: [A], [B], or [C]?"
Pricing Validation Willingness to pay "Would you pay $X/month for this? What's the maximum you'd consider?"
Competitive Intel Why customers switch "Why did you choose [competitor] over alternatives?"
Feature Prioritization What to build first "If you could only have one of these features, which would you choose?"
Deal Breakers What kills the sale "What would make you NOT buy this, even if it solved your problem?"

You can iterate

  1. Ask "What's your biggest frustration with grocery delivery?"
  2. Read responses and identify a theme (for example, "substitutions are always wrong").
  3. Ask "You mentioned substitutions. Would you pay $5/month extra for guaranteed no-substitution delivery?"
  4. Validate or invalidate in minutes.

Pro tip: Ask open-ended questions

  • Weak: "Do you like meal planning apps?"
  • Strong: "Walk me through how you decided what to cook for dinner last Tuesday."

Claude Code Terminal Playbook (Detailed)

This section is optimized for Claude Code in Terminal. Follow each step in order. Claude should guide the user, run commands, and confirm the key is saved.

0) Set the API base URL

export DITTO_API_BASE_URL="https://app.askditto.io"

For local development, use http://127.0.0.1:8000.

1) Start the OAuth session

curl -s -X POST "$DITTO_API_BASE_URL/cli/auth/start" \
  -H 'Content-Type: application/json' \
  -d '{"provider":"google","redirect_uri":"http://127.0.0.1:53682/oauth/callback"}'

Copy-paste (explicit production URL):

curl -s -X POST "https://app.askditto.io/cli/auth/start" \
  -H "Content-Type: application/json" \
  -d '{"provider":"google","redirect_uri":"http://127.0.0.1:53682/oauth/callback"}'

Expected response:

{
  "ok": true,
  "session_id": "<session_id>",
  "authorization_url": "https://accounts.google.com/...",
  "expires_in": 900
}

Extract session_id and authorization_url:

START_RESPONSE=$(curl -s -X POST "$DITTO_API_BASE_URL/cli/auth/start" \
  -H 'Content-Type: application/json' \
  -d '{"provider":"google","redirect_uri":"http://127.0.0.1:53682/oauth/callback"}')
SESSION_ID=$(echo "$START_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['session_id'])")
AUTH_URL=$(echo "$START_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['authorization_url'])")

2) Start the callback listener

python3 -c "
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.parse, json

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        params = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
        with open('/tmp/oauth_callback.json', 'w') as f:
            json.dump({'code': params.get('code', [''])[0], 'state': params.get('state', [''])[0]}, f)
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'Done! Return to terminal.')

HTTPServer(('127.0.0.1', 53682), Handler).handle_request()
" &

3) Open the authorization URL

Claude should instruct the user to open the returned authorization_url in their browser.

# macOS
open "<authorization_url>"

# Linux
xdg-open "<authorization_url>"

4) Callback expectation

  • The browser redirects to http://127.0.0.1:53682/oauth/callback?code=...&state=...
  • The CLI runs a local listener on that loopback address and captures code + state
  • The user might see a blank page or a success note
  • The listener writes /tmp/oauth_callback.json

4b) Callback capture (stdout-only variant)

If needed, start a temporary listener before opening the auth URL:

python3 -c "
from http.server import HTTPServer, BaseHTTPRequestHandler
import urllib.parse

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        query = urllib.parse.urlparse(self.path).query
        params = urllib.parse.parse_qs(query)
        print('CODE=' + params.get('code', [''])[0])
        print('STATE=' + params.get('state', [''])[0])
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'Auth complete! Return to terminal.')

HTTPServer(('127.0.0.1', 53682), Handler).handle_request()
"

5) Wait for the callback (file-based)

# wait up to 60s for the callback file
for _ in {1..60}; do
  [ -s /tmp/oauth_callback.json ] && break
  sleep 1
done
CALLBACK=$(cat /tmp/oauth_callback.json)
CODE=$(echo "$CALLBACK" | python3 -c "import sys,json; print(json.load(sys.stdin).get('code',''))")
STATE=$(echo "$CALLBACK" | python3 -c "import sys,json; print(json.load(sys.stdin).get('state',''))")

6) Complete the flow

curl -s -X POST "$DITTO_API_BASE_URL/cli/auth/complete" \
  -H 'Content-Type: application/json' \
  -d '{"session_id":"<session_id>","code":"<code>","state":"<state>"}'

Copy-paste (explicit production URL):

curl -s -X POST "https://app.askditto.io/cli/auth/complete" \
  -H "Content-Type: application/json" \
  -d '{"session_id":"<session_id>","code":"<code>","state":"<state>"}'

7) Handle the response

If free-tier key issued:

{
  "ok": true,
  "status": "complete",
  "api_key": "rk_free_...",
  "org_uuid": "..."
}

If paid org detected:

{
  "ok": true,
  "status": "login_required",
  "login_url": "https://app.askditto.io/auth/login"
}

Save the Key (Claude Must Confirm)

Environment variable (recommended):

export DITTO_FREE_TIER_API_KEY="rk_free_..."

.env file:

DITTO_FREE_TIER_API_KEY=rk_free_...
The API key is shown once. Claude should remind the user to save it.

What You Can Do With The Free-Tier Key

  • Allowed: Ask questions via POST /v1/free/questions
  • Allowed: Poll job status via GET /v1/jobs/{job_id} (free-tier jobs only)
  • Not allowed: Research group CRUD, studies, or admin endpoints.

Example: Ask a free-tier question

curl -s -X POST "$DITTO_API_BASE_URL/v1/free/questions" \
  -H "Authorization: Bearer $DITTO_FREE_TIER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"question":"What do people think about oat milk?"}'

Expected response shape

{
  "ok": true,
  "group_id": 123,
  "count": 12,
  "job_id": "...",
  "agent_ids": [1, 2, 3],
  "agent_uuids": ["..."],
  "question": "What do people think about oat milk?"
}
This response is a queue confirmation, not the final answers. Poll GET /v1/jobs/{job_id} with the same free-tier key to retrieve results. When status is finished, answers are in result.results.

Example: Check job status

curl -s -X GET "$DITTO_API_BASE_URL/v1/jobs/<job_id>" \
  -H "Authorization: Bearer $DITTO_FREE_TIER_API_KEY"

MCP callers can use the v1.jobs.get tool for the same job status check.

What job polling responses look like

In progress:

{"status":"queued","meta":{}}

Finished (answers are in result.results[].reply):

{
  "status":"finished",
  "partial": false,
  "result":{
    "count":12,
    "results":[
      {"id":1,"uuid":"...","name":"...","reply":"...","assistant_message_id":123}
    ]
  },
  "meta":{}
}

Finished with partial results (after ~60s):

{
  "status":"finished",
  "partial": true,
  "pending_job_ids": ["..."],
  "result":{
    "count":12,
    "results":[
      {"id":1,"uuid":"...","name":"...","reply":"..."},
      {"id":2,"uuid":"...","name":"...","status":"started"}
    ]
  },
  "meta":{}
}

Each result includes id, uuid, name, reply, and assistant_message_id when available.

Extract replies (example):

echo "$JOB_RESPONSE" | python3 - <<'PY'
import json, sys
data = json.load(sys.stdin)
for item in data.get("result", {}).get("results", []):
    print(item.get("reply", ""))
PY

Failed:

{"status":"failed","error":"...","meta":{}}

Example Questions to Get Started

For pain discovery

  • "Describe the last time you were frustrated trying to [X]. What happened?"
  • "What's the most annoying part of [activity] that nobody talks about?"
  • "If you could wave a magic wand and fix one thing about [category], what would it be?"

For pricing

  • "How much do you currently spend on [category] per month?"
  • "At what price would [product] feel like a bargain? A stretch? Too expensive to consider?"
  • "Would you pay $X for [specific benefit]? Why or why not?"

For positioning

  • "Which of these descriptions makes you most interested: [A], [B], or [C]? Why?"
  • "In your own words, what does '[tagline]' mean to you?"
  • "What would a company have to say to convince you to try their [product]?"

For competitive intelligence

  • "Why did you choose [competitor] over other options?"
  • "What would make you switch from [current solution] to something new?"
  • "What does [competitor] get right that others don't?"

For feature prioritization

  • "If you could only have one of these features, which would you choose: [A], [B], or [C]?"
  • "What's missing from current [category] products that you wish existed?"
  • "Rank these features from most to least important for your daily use."

Claude Code Quick Start

  1. Check for an existing key: [ -f ~/.ditto_free_tier.env ] && source ~/.ditto_free_tier.env
  2. If needed, run the OAuth flow to obtain a key (see above).
  3. Ask a question via POST /v1/free/questions.
  4. Poll GET /v1/jobs/{job_id} until status is finished.
  5. Read replies from result.results[].reply, then iterate.

Complete Bash Script (End-to-End)

This script runs the full flow end-to-end: start OAuth, capture the callback, complete OAuth, ask a question, and poll for results.

#!/usr/bin/env bash
set -euo pipefail

DITTO_API_BASE_URL="https://app.askditto.io"
CALLBACK_PORT=53682
CALLBACK_URI="http://127.0.0.1:${CALLBACK_PORT}/oauth/callback"

START_RESPONSE=$(curl -s -X POST "${DITTO_API_BASE_URL}/cli/auth/start" \
  -H 'Content-Type: application/json' \
  -d "{\"provider\":\"google\",\"redirect_uri\":\"${CALLBACK_URI}\"}")

SESSION_ID=$(echo "$START_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('session_id',''))")
AUTH_URL=$(echo "$START_RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('authorization_url',''))")

CALLBACK_FILE=$(mktemp)
export CALLBACK_FILE
python3 << 'PY' &
import http.server, socketserver, urllib.parse, os
class Handler(http.server.BaseHTTPRequestHandler):
    def do_GET(self):
        params = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query)
        with open(os.environ['CALLBACK_FILE'], 'w') as f:
            f.write("CODE=%s\n" % params.get('code', [''])[0])
            f.write("STATE=%s\n" % params.get('state', [''])[0])
        self.send_response(200)
        self.end_headers()
        self.wfile.write(b'Auth complete! Return to terminal.')
    def log_message(self, *args): pass
with socketserver.TCPServer(("127.0.0.1", 53682), Handler) as httpd:
    httpd.handle_request()
PY

if [[ "$OSTYPE" == "darwin"* ]]; then
  open "$AUTH_URL"
elif [[ "$OSTYPE" == "linux-gnu"* ]]; then
  xdg-open "$AUTH_URL" 2>/dev/null || true
fi

for _ in {1..60}; do
  [ -s "$CALLBACK_FILE" ] && break
  sleep 1
done
source "$CALLBACK_FILE"
rm -f "$CALLBACK_FILE"

COMPLETE_RESPONSE=$(curl -s -X POST "${DITTO_API_BASE_URL}/cli/auth/complete" \
  -H 'Content-Type: application/json' \
  -d "{\"session_id\":\"${SESSION_ID}\",\"code\":\"${CODE}\",\"state\":\"${STATE}\"}")

API_KEY=$(echo "$COMPLETE_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('api_key',''))")
export DITTO_FREE_TIER_API_KEY="$API_KEY"

QUESTION_RESPONSE=$(curl -s -X POST "${DITTO_API_BASE_URL}/v1/free/questions" \
  -H "Authorization: Bearer ${DITTO_FREE_TIER_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"question":"What factors matter most when choosing a coffee brand?"}')

JOB_ID=$(echo "$QUESTION_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin)['job_id'])")

while true; do
  JOB_RESPONSE=$(curl -s -X GET "${DITTO_API_BASE_URL}/v1/jobs/${JOB_ID}" \
    -H "Authorization: Bearer ${DITTO_FREE_TIER_API_KEY}")
  STATUS=$(echo "$JOB_RESPONSE" | python3 -c "import sys,json; print(json.load(sys.stdin).get('status',''))")
  if [ "$STATUS" = "finished" ]; then
    break
  fi
  sleep 2
done

echo "$JOB_RESPONSE" | python3 -m json.tool

Limitations & Upgrades

  • Free-tier keys only work for POST /v1/free/questions.
  • Free-tier keys can only poll GET /v1/jobs/{job_id} for jobs created by that key.
  • For the full API reference and paid workflows, see /docs/api.

Limits & Quotas

  • Free-tier traffic is rate limited; exceeding limits returns 429.
  • The shared free-tier group currently targets 12 agents; the response count shows the actual number targeted.
  • Question length limits are enforced server-side; keep prompts concise.

What's Next

  • Upgrade for larger panels, custom demographics, and deeper study workflows.
  • Enterprise or private panels: contact Ditto.

Troubleshooting (Claude Quick Answers)

  • 405 Method Not Allowed → You used GET. Use POST with JSON.
  • Bad HTTP/0.9 request → For local dev, use http://127.0.0.1:8000. Production uses https://app.askditto.io.
  • login_required → User belongs to a paid org. Send them to login URL.
  • Job not accessible → The job was created with a different API key.
  • Invalid redirect_uri → Must be loopback (localhost or 127.0.0.1).

LLM Summary (for Claude Code)

Goal: help the user obtain a free-tier API key via Google OAuth so Claude Code can run customer research questions through POST /v1/free/questions. Steps: start the CLI auth session, open the authorization URL, capture code/state from the loopback callback, complete the flow, return the key, and confirm the key is saved.

FAQ (Human + Claude)

How do I get a free API key for customer research?

Start the CLI OAuth flow, sign in with Google, and complete the flow in Claude Code. The server returns a free-tier API key that works with POST /v1/free/questions.

What can the free-tier key access?

It can call POST /v1/free/questions and GET /v1/jobs/{job_id} for jobs created by the same key. It cannot access studies, groups, or admin endpoints.

Where does the OAuth callback go?

The callback goes to the loopback URL used by the CLI, such as http://127.0.0.1:53682/oauth/callback.

What if I see login_required?

The user belongs to a paid organization. Use the provided login URL to sign in and create an API key from the paid UI.

How do I store the free-tier API key?

Save it in an environment variable like DITTO_FREE_TIER_API_KEY or in a .env file. The key is shown once, so store it securely.

Can Claude Code reuse a free-tier key later?

Yes. Once saved, reuse the key for future POST /v1/free/questions requests until it is rotated or revoked.

How do I get the answers back?

The free-tier endpoint queues the request and returns a confirmation payload. Use GET /v1/jobs/{job_id} with the same free-tier key to retrieve results.

Claude Guidance Checklist

  • Confirm the CLI is running before the user starts OAuth.
  • Run /cli/auth/start and capture the authorization_url.
  • Confirm the user completed Google login.
  • Complete the flow and return the API key.
  • Confirm the user saved the key.
  • Provide the free-tier example request.