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:
Copied to clipboard!
Jump to the bash script: Complete Working Example
What happens
- Opens Google sign-in in your browser
- Authenticates and captures your API key
- Saves the key to
~/.ditto_free_tier.env - 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/startreturnssession_idandauthorization_url.POST /cli/auth/completereturnsapi_keyorlogin_required.POST /v1/free/questionsqueues a question and returnsjob_id.GET /v1/jobs/{job_id}returns answers whenstatusisfinishedinresult.results[].replyand includespartialif 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?
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.
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.
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)
Paste the command above to start the OAuth session.
Sign in with Google and approve the consent screen.
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 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
- Ask "What's your biggest frustration with grocery delivery?"
- Read responses and identify a theme (for example, "substitutions are always wrong").
- Ask "You mentioned substitutions. Would you pay $5/month extra for guaranteed no-substitution delivery?"
- 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_...
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?"
}
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
- Check for an existing key: [ -f ~/.ditto_free_tier.env ] && source ~/.ditto_free_tier.env
- If needed, run the OAuth flow to obtain a key (see above).
- Ask a question via POST /v1/free/questions.
- Poll GET /v1/jobs/{job_id} until status is finished.
- 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.