SDKsTypeScript

TypeScript SDK

Official TypeScript/JavaScript SDK for FirstHandAPI.

Installation

npm install @firsthandapi/sdk

View on npm →

Quick 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 verificationverifyWebhookSignature() with timing-safe comparison

Methods

Jobs

  • createJob(body) — Post a content collection job
  • getJob(jobId) — Get job by ID
  • listJobs(params?) — List jobs with filters
  • cancelJob(jobId) — Cancel an open job
  • getJobFiles(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 jobs
  • getSubmission(submissionId) — Get submission by ID
  • getSubmissionSummary(params?) — Aggregate submission metrics

API Keys

  • createApiKey(body) — Create a new API key
  • listApiKeys() — List API keys
  • rotateApiKey(keyId) — Rotate with 24h overlap
  • revokeApiKey(keyId) — Revoke immediately

Webhooks

  • createWebhookEndpoint(body) — Create endpoint
  • listWebhookEndpoints() — List endpoints
  • getWebhookEndpoint(id) — Get endpoint
  • updateWebhookEndpoint(id, body) — Update endpoint
  • deleteWebhookEndpoint(id) — Delete endpoint
  • rotateWebhookSecret(id) — Rotate signing secret

Webhook Events

  • listWebhookEvents() — List events
  • replayWebhookEvent(id) — Replay an event

Billing

  • getCreditBalance() — Current credit balance
  • listTransactions() — Transaction history
  • purchaseCredits(body) — Initiate Stripe Checkout

Organization

  • getSettings() — Get org settings
  • updateSettings(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);