Quickstart

Quickstart

Post your first content collection job in 5 minutes.

0. Create an Account

POST /v1/auth/signup
curl -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/sdk
pip install firsthandapi

3. 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

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 verification
endpoint = client.create_webhook_endpoint({
    "url": "https://your-app.com/webhooks/firsthand",
    "events": ["submission.approved", "job.completed"],
})
# Save endpoint["secret"] for signature verification

Then 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', 200

See 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 SizeExpected Turnaround
1-10 filesMinutes to hours
10-50 filesHours to 1 day
50-500 files1-3 days
500+ files3-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:

ScoreMeaning
5 starsExceptional quality, exactly what was requested
4 starsGood quality, meets requirements
3 starsAcceptable quality, auto-approved
2 starsBelow threshold, auto-rejected
1 starPoor 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 constraints
  • scoring_criteria: Custom rubric injected into AI scoring (e.g., “Penalize shaky footage”)

Next steps