Quickstart
Post your first content collection job in 5 minutes.
0. Create an Account
POST /v1/auth/signupcurl -X POST https://api.firsthandapi.com/v1/auth/signup \
-H "Content-Type: application/json" \
-d '{
"name": "Your Org Name",
"email": "you@example.com"
}'Response: 200 OK
{
"organization_id": "org_01JQ...",
"api_key": "fh_live_...",
"credits_cents": 500
}You’ll receive $2.50 in free credits to get started (after phone verification). Save your api_key — it is only shown once.
1. Get your API key
Sign up at firsthandapi.com and create an API key from the dashboard.
Your key will look like fh_live_... (production) or fh_test_... (sandbox).
2. Install an SDK
npm install @firsthandapi/sdkpip install firsthandapi3. Post a job
import { FirstHandClient, FirstHandApiError } from '@firsthandapi/sdk';
const client = new FirstHandClient({ apiKey: 'fh_live_...' });
try {
const job = await client.createJob({
type: 'data_collection',
description: 'Take a clear photo of any coffee cup from above. Must show the full cup.',
files_needed: 50,
accepted_formats: ['image/jpeg', 'image/png'],
price_per_file_cents: 75,
});
console.log(job.id); // job_01JQ...
console.log(job.status); // open
} catch (e) {
if (e instanceof FirstHandApiError) {
console.error(`${e.status} ${e.type}: ${e.message}`);
}
}from firsthandapi import FirstHandClient, FirstHandApiError
client = FirstHandClient(api_key="fh_live_...")
try:
job = client.create_job({
"type": "data_collection",
"description": "Take a clear photo of any coffee cup from above. Must show the full cup.",
"files_needed": 50,
"accepted_formats": ["image/jpeg", "image/png"],
"price_per_file_cents": 75,
})
print(job["id"]) # job_01JQ...
print(job["status"]) # open
except FirstHandApiError as e:
print(f"{e.status} {e.type}: {e}")4. Get submissions
Option A: Webhook (recommended)
Register a webhook endpoint to receive real-time notifications. URLs must use HTTPS.
const endpoint = await client.createWebhookEndpoint({
url: 'https://your-app.com/webhooks/firsthand',
events: ['submission.approved', 'job.completed'],
});
// Save endpoint.secret for signature verificationendpoint = client.create_webhook_endpoint({
"url": "https://your-app.com/webhooks/firsthand",
"events": ["submission.approved", "job.completed"],
})
# Save endpoint["secret"] for signature verificationThen handle incoming events:
import { verifyWebhookSignature } from '@firsthandapi/sdk';
app.post('/webhooks/firsthand', (req, res) => {
verifyWebhookSignature(req.rawBody, req.headers['x-firsthand-signature'], WEBHOOK_SECRET);
const event = req.body;
if (event.type === 'submission.approved') {
console.log(`File approved: ${event.data.submission_id}`);
} else if (event.type === 'job.completed') {
console.log(`Job done! ${event.data.files_approved} files ready`);
}
res.status(200).send('ok');
});from firsthandapi import verify_webhook_signature
@app.route('/webhooks/firsthand', methods=['POST'])
def handle_webhook():
verify_webhook_signature(request.data, request.headers['X-FirstHand-Signature'], WEBHOOK_SECRET)
event = request.json
if event['type'] == 'submission.approved':
print(f"File approved: {event['data']['submission_id']}")
return 'ok', 200See Webhook Handling for full payload schemas and best practices.
Option B: Poll for files
const files = await client.getJobFiles('job_01JQ...');
for (const file of files.data) {
console.log(`${file.original_filename}: ${file.download_url}`);
}files = client.get_job_files("job_01JQ...")
for f in files["data"]:
print(f"{f['original_filename']}: {f['download_url']}")Each file includes an annotations object with auto-generated metadata (object labels, OCR, scene classification, color palettes, speaker counts, transcripts, and more). See the Auto-Labeling guide for full schema details.
5. What to Expect
After posting a job, workers see it in the mobile app and start submitting files. Typical response times:
| Job Size | Expected Turnaround |
|---|---|
| 1-10 files | Minutes to hours |
| 10-50 files | Hours to 1 day |
| 50-500 files | 1-3 days |
| 500+ files | 3-7 days |
Turnaround varies by price, location constraints, and content complexity. Higher price_per_file_cents attracts workers faster.
5. AI Quality Scores
Every submission is automatically scored by a multi-model AI ensemble:
| Score | Meaning |
|---|---|
| 5 stars | Exceptional quality, exactly what was requested |
| 4 stars | Good quality, meets requirements |
| 3 stars | Acceptable quality, auto-approved |
| 2 stars | Below threshold, auto-rejected |
| 1 star | Poor quality or irrelevant, auto-rejected |
Files scoring 3+ stars are auto-approved and delivered to your job folder.
6. Quality Constraints (Optional)
For higher-quality data, add constraints to your job:
const job = await client.createJob({
type: 'data_collection',
description: 'Record a 30-second video walkthrough of your living room.',
files_needed: 20,
accepted_formats: ['video/mp4'],
price_per_file_cents: 200,
min_width: 1280, // Reject videos below 720p
min_height: 720,
min_duration_seconds: 20, // Minimum 20 seconds
max_duration_seconds: 60, // Maximum 60 seconds
worker_guidelines: 'Hold phone horizontally. Do NOT speak. Pan slowly and steadily.',
});min_width/min_height: Auto-reject submissions below this resolution before AI scoring (no credits consumed)worker_guidelines: Shown to workers separately from the description — use for strict constraintsscoring_criteria: Custom rubric injected into AI scoring (e.g., “Penalize shaky footage”)
Next steps
- Writing Job Descriptions — Get better files with clear descriptions
- AI Agent Integration — MCP server setup for Claude Code
- Credit Billing — Fund your account and manage billing
- Webhook Handling — Signature verification and best practices