TypeScript SDK
Official TypeScript/JavaScript SDK for FirstHandAPI.
Installation
npm install @firsthandapi/sdkQuick Start
import { FirstHandClient } from '@firsthandapi/sdk';
const client = new FirstHandClient({ apiKey: 'fh_live_...' });
// Post a job with quality constraints
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,
min_width: 1280, // auto-reject images below 720p
min_height: 720,
worker_guidelines: 'Hold phone horizontally. Natural lighting only. No filters.',
});
// Get approved files
const files = await client.getJobFiles(job.id);Configuration
const client = new FirstHandClient({
apiKey: 'fh_live_...',
baseUrl: 'https://api.firsthandapi.com', // default
timeout: 30_000, // 30s default
maxRetries: 3, // default
});Features
- Auto-retry — Exponential backoff with jitter on 429 and 5xx errors
- Idempotency — Automatic UUID generation for all POST requests
- Webhook verification —
verifyWebhookSignature()with timing-safe comparison
Methods
Jobs
createJob(body)— Post a content collection jobgetJob(jobId)— Get job by IDlistJobs(params?)— List jobs with filterscancelJob(jobId)— Cancel an open jobgetJobFiles(jobId, params?)— Get approved files for a job (includes auto-generated annotations)getJobSubmissions(jobId, params?)— List all submissions for a job
Submissions
listSubmissions(params?)— List submissions across all jobsgetSubmission(submissionId)— Get submission by IDgetSubmissionSummary(params?)— Aggregate submission metrics
API Keys
createApiKey(body)— Create a new API keylistApiKeys()— List API keysrotateApiKey(keyId)— Rotate with 24h overlaprevokeApiKey(keyId)— Revoke immediately
Webhooks
createWebhookEndpoint(body)— Create endpointlistWebhookEndpoints()— List endpointsgetWebhookEndpoint(id)— Get endpointupdateWebhookEndpoint(id, body)— Update endpointdeleteWebhookEndpoint(id)— Delete endpointrotateWebhookSecret(id)— Rotate signing secret
Webhook Events
listWebhookEvents()— List eventsreplayWebhookEvent(id)— Replay an event
Billing
getCreditBalance()— Current credit balancelistTransactions()— Transaction historypurchaseCredits(body)— Initiate Stripe Checkout
Organization
getSettings()— Get org settingsupdateSettings(body)— Update org settings
Examples
Error Handling
import { FirstHandClient, FirstHandApiError, FirstHandRateLimitError } from '@firsthandapi/sdk';
const client = new FirstHandClient({ apiKey: 'fh_live_...' });
try {
const job = await client.createJob({
type: 'data_collection',
description: 'Photos of storefronts',
files_needed: 10,
accepted_formats: ['image/jpeg'],
price_per_file_cents: 50,
});
} catch (err) {
if (err instanceof FirstHandRateLimitError) {
console.log(`Rate limited. Retry after ${err.retryAfter}s`);
} else if (err instanceof FirstHandApiError) {
console.log(err.status); // 400
console.log(err.type); // "validation_error"
console.log(err.requestId); // "req_01JQ..."
console.log(err.retryable); // false
}
}Webhook Verification
import { verifyWebhookSignature } from '@firsthandapi/sdk';
app.post('/webhooks/firsthand', (req, res) => {
try {
verifyWebhookSignature({
payload: req.body,
signature: req.headers['x-firsthandapi-signature'] as string,
secret: 'whsec_...',
});
} catch {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
switch (event.type) {
case 'submission.approved':
console.log(`File approved for job ${event.data.job_id}`);
break;
case 'job.completed':
console.log(`Job ${event.data.job_id} completed with ${event.data.files_approved} files`);
break;
}
res.status(200).send('ok');
});Billing
// Check balance
const balance = await client.getCreditBalance();
console.log(`Balance: $${(balance.available_cents / 100).toFixed(2)}`);
// Purchase credits
const checkout = await client.purchaseCredits({
amount_cents: 5000, // $50.00
});
console.log(`Checkout URL: ${checkout.checkout_url}`);
// List transactions
const txns = await client.listTransactions({ limit: 10 });
for (const txn of txns.data) {
console.log(`${txn.type}: ${txn.amount_cents}c — ${txn.description}`);
}Submissions
// List submissions for a job
const submissions = await client.getJobSubmissions('job_01JQ...');
for (const sub of submissions.data) {
console.log(`${sub.id}: ${sub.status} — ${sub.ai_star_rating}★`);
}
// Get approved files with annotations
const files = await client.getJobFiles('job_01JQ...');
for (const file of files.data) {
console.log(`${file.original_filename} (${file.content_type})`);
console.log(` Download: ${file.download_url}`);
console.log(` Rating: ${file.ai_star_rating}★`);
if (file.annotations) {
console.log(` Labels: ${JSON.stringify(file.annotations)}`);
}
}Pagination
let cursor: string | undefined;
do {
const page = await client.listJobs({ cursor, limit: 20 });
for (const job of page.data) {
console.log(`${job.id}: ${job.status}`);
}
cursor = page.has_more ? (page.next_cursor ?? undefined) : undefined;
} while (cursor);