Pricing Enterprise Docs Agent Skills Learn

REST API

Integrate Runhuman directly from your backend, scripts, or any HTTP client.


Authentication

Include your API key or Personal Access Token (PAT) in the Authorization header:

Authorization: Bearer YOUR_API_KEY_OR_PAT

Two token types:

  • API Keys — Scoped to an organization. Create from your organization’s API Keys page in the Dashboard. Best for CI/CD and server-to-server integrations.
  • Personal Access Tokens (PATs) — Scoped to your user account. Create from Settings > Tokens. Best for CLI tools and personal scripts.

Both work with all job creation endpoints. API keys automatically resolve the organization context; PATs require you to specify projectId or organizationId.


Synchronous Endpoint

POST /api/run creates a test and waits for completion. The request blocks until a human tester finishes (up to 60 minutes).

If the test does not complete within 60 minutes, the request returns 408 Timeout.


Asynchronous Endpoints

For longer tests or parallel testing, use the async pattern:

Step 1: Create a job

POST /api/jobs creates a test and returns immediately with a job ID.

const response = await fetch('https://runhuman.com/api/jobs', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    url: 'https://myapp.com/checkout',
    description: 'Complete the full checkout flow',
    projectId: PROJECT_ID,
    targetDurationMinutes: 10,
    outputSchema: {
      checkoutWorks: { type: 'boolean', description: 'Order placed successfully?' }
    }
  })
});

const { jobId } = await response.json();
console.log('Job created:', jobId);

Step 2: Poll for results

GET /api/jobs/:jobId retrieves the job status and results.

async function pollJob(jobId) {
  while (true) {
    const response = await fetch(`https://runhuman.com/api/jobs/${jobId}`, {
      headers: { 'Authorization': `Bearer ${API_KEY}` }
    });
    const job = await response.json();

    if (job.status === 'completed') {
      return job;
    }

    if (['incomplete', 'abandoned', 'rejected', 'error'].includes(job.status)) {
      throw new Error(`Job failed: ${job.status}`);
    }

    await new Promise(resolve => setTimeout(resolve, 30000));
  }
}

const result = await pollJob(jobId);
console.log(result.result.data);

Request Parameters

These parameters apply to POST /api/jobs and POST /api/run.

ParameterTypeRequiredDefaultDescription
projectIdstringConditional-Project ID. Required unless using an org-scoped API key with githubRepo for auto-creation
organizationIdstringNo-Organization ID. Used with PATs when projectId is not provided, paired with githubRepo for project auto-creation
urlstringConditional-URL for the tester to visit. Required for simple jobs; optional when using prNumbers, issueNumbers, or template
descriptionstringConditional-Instructions for the tester. Required for simple jobs; optional when using prNumbers, issueNumbers, or template
outputSchemaobjectNo-JSON Schema defining data to extract. If omitted, only success/explanation returned
resultsTemplatestringNo-MDForm template for free-form text reports (alternative to outputSchema)
templatestringNo-Template name to use as base configuration. See Templates.
templateContentstringNo-Raw template content (markdown with YAML frontmatter). See Templates.
targetDurationMinutesnumberNo30Time limit (1-60 minutes)
allowDurationExtensionbooleanNotrueAllow tester to request more time
maxExtensionMinutesnumber/falseNofalseMaximum extension allowed. false means unlimited
additionalValidationInstructionsstringNo-Custom instructions for AI validation
deviceClassstringNodesktop"desktop" or "mobile"
githubRepostringNo-GitHub repo ("owner/repo") for AI context. Required when using prNumbers or issueNumbers

| githubToken | string | No | - | GitHub token for operations without GitHub App installation | | prNumbers | number[] | No | - | PR numbers for AI test plan generation (async only) | | issueNumbers | number[] | No | - | Issue numbers for AI test plan generation (async only) | | checkTestability | boolean | No | true* | Reject job early if AI determines it’s not testable. *Default true only when prNumbers/issueNumbers are provided | | attachments | array | No | - | Media attachments for additional context (max 10). Type auto-detected from URL | | metadata | object | No | - | Custom metadata for tracking job source and context |

How url and description requirements work:

  • Simple jobs (no PR/issue numbers, no template): Both url and description are required
  • PR/issue jobs (prNumbers or issueNumbers): url is required, description is auto-generated by AI
  • Template jobs (template or templateContent): Values come from the template; you can override with explicit fields

See Reference for full details on output schema format and response fields.


Handling Responses

Synchronous Response (POST /api/run)

A completed synchronous test returns:

{
  "id": "job_abc123",
  "status": "completed",
  "result": {
    "success": true,
    "explanation": "Login worked correctly. User was redirected to dashboard.",
    "data": {
      "loginWorks": true,
      "redirectsToDashboard": true
    }
  },
  "costUsd": 0.18,
  "testDurationSeconds": 100,
  "testerResponse": "I entered the credentials and clicked login...",
  "testerAlias": "Phoenix",
  "testerAvatarUrl": "https://images.subscribe.dev/uploads/.../phoenix.png",
  "testerData": {
    "screenshots": ["https://..."],
    "videoUrl": "https://..."
  }
}

Async Job Response (GET /api/jobs/:jobId)

Polling a completed async job returns additional fields:

{
  "id": "job_abc123",
  "status": "completed",
  "result": {
    "success": true,
    "explanation": "Login worked correctly. User was redirected to dashboard.",
    "data": {
      "loginWorks": true,
      "redirectsToDashboard": true
    }
  },
  "costUsd": 0.18,
  "testDurationSeconds": 100,
  "testerResponse": "I entered the credentials and clicked login...",
  "testerAlias": "Phoenix",
  "testerAvatarUrl": "https://images.subscribe.dev/uploads/.../phoenix.png",
  "testerData": {
    "screenshots": ["https://..."],
    "videoUrl": "https://..."
  },
  "jobUrl": "https://runhuman.com/dashboard/proj_abc/jobs/job_abc123",
  "projectName": "My Web App",
  "keyMoments": []
}

Tester Identity Fields:

  • testerAlias: Anonymized tester name (e.g., “Phoenix”, “Atlas”, “Nova”)
  • testerAvatarUrl: Avatar image URL for displaying tester identity

These fields allow you to differentiate between testers while protecting their privacy.

Check status before accessing result. Only completed jobs have result data.


Testing a Checkout Flow

import requests

response = requests.post(
    'https://runhuman.com/api/run',
    headers={
        'Authorization': f'Bearer {API_KEY}',
        'Content-Type': 'application/json'
    },
    json={
        'url': 'https://myapp.com/products',
        'description': 'Add a product to cart, go to checkout, fill shipping info, verify the total is correct',
        'targetDurationMinutes': 10,
        'outputSchema': {
            'checkoutCompletes': { 'type': 'boolean', 'description': 'Checkout flow completes without errors?' },
            'totalCorrect': { 'type': 'boolean', 'description': 'Order total displays correctly?' },
            'issues': { 'type': 'array', 'description': 'Any issues found' }
        }
    }
)

result = response.json()
print(f"Checkout works: {result['result']['data']['checkoutCompletes']}")
print(f"Issues: {result['result']['data']['issues']}")

Custom Validation Instructions

Use additionalValidationInstructions to guide how AI interprets results:

const response = await fetch('https://runhuman.com/api/run', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    url: 'https://myapp.com/checkout',
    description: 'Complete checkout with test card 4242424242424242',
    outputSchema: {
      orderPlaced: { type: 'boolean', description: 'Order was placed?' },
      confirmationShown: { type: 'boolean', description: 'Confirmation number displayed?' }
    },
    additionalValidationInstructions: `
      Ignore minor UI glitches in the header.
      Focus only on whether the order was placed and confirmation shown.
      If tester mentions payment errors, mark orderPlaced as false even if they eventually succeeded.
    `
  })
});

Running Tests in Parallel

Create multiple jobs and poll them concurrently:

const urls = [
  'https://myapp.com/page1',
  'https://myapp.com/page2',
  'https://myapp.com/page3'
];

const jobIds = await Promise.all(
  urls.map(async (url) => {
    const res = await fetch('https://runhuman.com/api/jobs', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        url,
        description: 'Check if page loads and has no broken images',
        outputSchema: {
          pageLoads: { type: 'boolean', description: 'Page loads?' },
          brokenImages: { type: 'boolean', description: 'Any broken images?' }
        }
      })
    });
    const data = await res.json();
    return data.jobId;
  })
);

const results = await Promise.all(jobIds.map(pollJob));

Wrapper Function

A reusable function for your codebase:

async function runHumanTest(url, description, outputSchema, options = {}) {
  const response = await fetch('https://runhuman.com/api/run', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.RUNHUMAN_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      url,
      description,
      outputSchema,
      targetDurationMinutes: options.targetDurationMinutes || 30
    })
  });

  if (!response.ok) {
    if (response.status === 408) {
      throw new Error('Test timed out after 60 minutes');
    }
    const error = await response.json();
    throw new Error(`API error: ${error.message}`);
  }

  return await response.json();
}

const result = await runHumanTest(
  'https://myapp.com/checkout',
  'Test the checkout flow',
  {
    checkoutWorks: { type: 'boolean', description: 'Checkout completed?' }
  }
);

Organizations API

Organizations are the top-level entity for billing, projects, and team management.

List Organizations

GET /api/organizations returns all organizations you belong to.

const response = await fetch('https://runhuman.com/api/organizations', {
  headers: { 'Authorization': `Bearer ${API_KEY}` }
});

const { items } = await response.json();
// items: [{ id, name, slug, projectCount, memberCount, createdAt }]

Get Organization

GET /api/organizations/:organizationId retrieves organization details.

const response = await fetch(
  `https://runhuman.com/api/organizations/${organizationId}`,
  { headers: { 'Authorization': `Bearer ${API_KEY}` } }
);

const org = await response.json();

Create Organization

POST /api/organizations creates a new organization.

const response = await fetch('https://runhuman.com/api/organizations', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({ name: 'My Team' })
});

const org = await response.json();

Organization Members

GET /api/organizations/:organizationId/members lists organization members.

const response = await fetch(
  `https://runhuman.com/api/organizations/${organizationId}/members`,
  { headers: { 'Authorization': `Bearer ${API_KEY}` } }
);

const { items } = await response.json();

Invite Member

POST /api/organizations/:organizationId/invite invites a user by email.

const response = await fetch(
  `https://runhuman.com/api/organizations/${organizationId}/invite`,
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ email: 'teammate@example.com', role: 'member' })
  }
);

const { inviteUrl } = await response.json();

Remove Member

DELETE /api/organizations/:organizationId/members/:userId removes a member from the organization.


Projects API

Manage your projects and organize test jobs. Projects belong to organizations.

Create Project

POST /api/projects creates a new project within an organization.

const response = await fetch('https://runhuman.com/api/projects', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'My Web App',
    organizationId: 'org_abc123',
    defaultUrl: 'https://myapp.com',
    githubRepo: 'owner/repo'
  })
});

const project = await response.json();
console.log('Project ID:', project.id);
ParameterTypeRequiredDescription
namestringYesProject name
organizationIdstringYesOrganization this project belongs to
defaultUrlstringNoDefault URL for testing
githubRepostringNoGitHub repository ("owner/repo")

List Projects

GET /api/projects returns all projects accessible to you.

const response = await fetch('https://runhuman.com/api/projects', {
  headers: { 'Authorization': `Bearer ${API_KEY}` }
});

const { items, pagination } = await response.json();

Get Project

GET /api/projects/:projectId retrieves project details.

const response = await fetch(`https://runhuman.com/api/projects/${projectId}`, {
  headers: { 'Authorization': `Bearer ${API_KEY}` }
});

const project = await response.json();

Update Project

PATCH /api/projects/:projectId updates project details.

const response = await fetch(`https://runhuman.com/api/projects/${projectId}`, {
  method: 'PATCH',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'Updated Name',
    defaultUrl: 'https://newurl.com'
  })
});

Delete Project

DELETE /api/projects/:projectId soft-deletes a project and all associated data.

const response = await fetch(`https://runhuman.com/api/projects/${projectId}`, {
  method: 'DELETE',
  headers: { 'Authorization': `Bearer ${API_KEY}` }
});

API Keys Management

Manage API keys for programmatic access. API keys are scoped to organizations.

List API Keys

GET /api/keys?organizationId=org_abc123 returns all keys for an organization.

const response = await fetch(`https://runhuman.com/api/keys?organizationId=${organizationId}`, {
  headers: { 'Authorization': `Bearer ${API_KEY}` }
});

const { items, pagination } = await response.json();

Create API Key

POST /api/keys creates a new organization-scoped API key.

const response = await fetch('https://runhuman.com/api/keys', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    organizationId: 'org_abc123',
    name: 'Production Key'
  })
});

const { id, key, name } = await response.json();
console.log('API Key:', key); // Only shown once!

Important: The full API key is only returned during creation. Store it securely.

Revoke API Key

POST /api/keys/:keyId/revoke revokes an API key.

const response = await fetch(`https://runhuman.com/api/keys/${keyId}/revoke`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${API_KEY}` }
});

Delete API Key

DELETE /api/keys/:keyId permanently deletes an API key.

const response = await fetch(`https://runhuman.com/api/keys/${keyId}`, {
  method: 'DELETE',
  headers: { 'Authorization': `Bearer ${API_KEY}` }
});

Personal Access Tokens (PATs)

PATs are user-scoped tokens for CLI tools and personal scripts. Unlike API keys, they’re tied to your user account, not an organization.

Create PAT

POST /api/pats creates a new Personal Access Token.

const response = await fetch('https://runhuman.com/api/pats', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${SESSION_TOKEN}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    name: 'CLI Token',
    expiresAt: '2026-12-31T23:59:59Z'
  })
});

const { id, token, name } = await response.json();
console.log('PAT:', token); // Only shown once! Starts with rh_pat_

Important: The full token is only returned during creation. Store it securely.

List PATs

GET /api/pats returns all your PATs.

const response = await fetch('https://runhuman.com/api/pats', {
  headers: { 'Authorization': `Bearer ${SESSION_TOKEN}` }
});

const { items, pagination } = await response.json();

Revoke PAT

POST /api/pats/:patId/revoke revokes a PAT.

Delete PAT

DELETE /api/pats/:patId permanently deletes a PAT.


Templates API

Create reusable test templates with predefined schemas.

List Templates

GET /api/projects/:projectId/templates returns templates for a project.

const response = await fetch(
  `https://runhuman.com/api/projects/${projectId}/templates`,
  { headers: { 'Authorization': `Bearer ${API_KEY}` } }
);

const { items, pagination } = await response.json();

Create Template

POST /api/projects/:projectId/templates creates a new template.

const response = await fetch(
  `https://runhuman.com/api/projects/${projectId}/templates`,
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      name: 'Smoke Test',
      description: 'Basic smoke test for new deployments',
      testInstructions: 'Check if homepage loads and key features work',
      outputSchema: {
        pageLoads: { type: 'boolean', description: 'Homepage loads?' },
        loginWorks: { type: 'boolean', description: 'Can user login?' },
        issues: { type: 'array', description: 'Any issues found?' }
      },
      targetDurationMinutes: 30
    })
  }
);

const template = await response.json();

Get Template

GET /api/projects/:projectId/templates/:templateId retrieves template details.

Update Template

PATCH /api/projects/:projectId/templates/:templateId updates a template.

Delete Template

DELETE /api/projects/:projectId/templates/:templateId deletes a template.


GitHub Integration

Integrate with GitHub for issue-based testing. GitHub repos are linked to projects via the GitHub App installation flow.

Connect GitHub

GET /api/github/oauth/authorize returns the GitHub App installation URL.

const response = await fetch(
  `https://runhuman.com/api/github/oauth/authorize?organizationId=${orgId}`,
  { headers: { 'Authorization': `Bearer ${API_KEY}` } }
);

After installation, repositories are accessible via the organization’s GitHub installations.

List Issues

GET /api/github/issues retrieves issues from a project’s linked repository.

const response = await fetch(
  `https://runhuman.com/api/github/issues?projectId=${projectId}&state=open&labels=bug`,
  { headers: { 'Authorization': `Bearer ${API_KEY}` } }
);

const { items, pagination } = await response.json();

Query Parameters:

  • projectId - (Required) Project with linked GitHub repo
  • state - Filter by state: open, closed, or all (default: open)
  • labels - Filter by labels (comma-separated)
  • assignee - Filter by assignee
  • sort - Sort field: created, updated, or comments
  • direction - Sort direction: asc or desc
  • page - Page number (default: 1)
  • per_page - Items per page (default: 30)

Get Issue

GET /api/github/issues/:issueNumber retrieves a single issue.

const response = await fetch(
  `https://runhuman.com/api/github/issues/123?projectId=${projectId}`,
  { headers: { 'Authorization': `Bearer ${API_KEY}` } }
);

const issue = await response.json();

Test Issue

POST /api/github/issues/test creates a test job for a specific GitHub issue.

const response = await fetch('https://runhuman.com/api/github/issues/test', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    projectId: 'proj_abc123',
    issueNumber: 123
  })
});

const { sessionId } = await response.json();

Bulk Test Issues

POST /api/github/issues/bulk-test starts a test session for multiple issues.

const response = await fetch('https://runhuman.com/api/github/issues/bulk-test', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    projectId: 'proj_abc123',
    issueNumbers: [123, 124, 125]
  })
});

const { sessionId } = await response.json();

Test Sessions

GET /api/github/issues/test-sessions lists test sessions for a project.

const response = await fetch(
  `https://runhuman.com/api/github/issues/test-sessions?projectId=${projectId}`,
  { headers: { 'Authorization': `Bearer ${API_KEY}` } }
);

const { sessions, pagination } = await response.json();

Create Issue from Finding

POST /api/jobs/:jobId/create-issue creates a GitHub issue from an AI-extracted finding on a completed job. After a job completes, Runhuman automatically extracts potential issues from the test results. You can then selectively create GitHub issues from these findings.

const response = await fetch(
  `https://runhuman.com/api/jobs/${jobId}/create-issue`,
  {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${AUTH_TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      issue: {
        title: 'Login button unresponsive on mobile',
        description: 'The login button does not respond to taps on mobile devices',
        reproductionSteps: ['Open the app on mobile', 'Navigate to login', 'Tap the login button'],
        severity: 'high',
        suggestedLabels: ['bug', 'mobile'],
      },
      githubRepo: 'owner/repo', // optional, falls back to job's githubRepo
    }),
  }
);

const { created, duplicateInfo } = await response.json();
console.log('Issue URL:', created.issueUrl);

The response includes duplicate detection results — if a similar issue already exists, Runhuman comments on it instead of creating a duplicate.


Authentication & User

Get information about the current authenticated user.

Get Current User

GET /api/auth/me returns the authenticated user’s details.

const response = await fetch('https://runhuman.com/api/auth/me', {
  headers: { 'Authorization': `Bearer ${API_KEY}` }
});

const { user, email } = await response.json();
console.log('User ID:', user.id);
console.log('Email:', email);

Billing

Check your organization’s credit balance and manage subscriptions.

Get Credit Balance

GET /api/billing/balance retrieves your organization’s current credit balance.

const response = await fetch('https://runhuman.com/api/billing/balance', {
  headers: { 'Authorization': `Bearer ${API_KEY}` }
});

const { balance, hasCredits, hasActiveSubscription } = await response.json();
console.log(`Balance: $${balance}`);

Check Credits Available

GET /api/billing/has-credits checks whether your organization has available credits.

const response = await fetch('https://runhuman.com/api/billing/has-credits', {
  headers: { 'Authorization': `Bearer ${API_KEY}` }
});

const { hasCredits } = await response.json();

Create Checkout Session

POST /api/billing/checkout creates a checkout session to purchase credits.

const response = await fetch('https://runhuman.com/api/billing/checkout', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    tier: 'pro',
    billingCycle: 'monthly'
  })
});

const { checkoutUrl } = await response.json();
// Redirect user to checkoutUrl

Get Subscription

GET /api/billing/subscription retrieves the active subscription details.

const response = await fetch(
  `https://runhuman.com/api/billing/subscription?organizationId=${orgId}`,
  { headers: { 'Authorization': `Bearer ${API_KEY}` } }
);

const { subscription } = await response.json();
// subscription: { id, tier, billingCycle, ... } or null

Change Plan

POST /api/billing/change-plan updates your subscription tier or billing cycle.

const response = await fetch('https://runhuman.com/api/billing/change-plan', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${API_KEY}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    tier: 'business',
    billingCycle: 'annual'
  })
});

Pagination

List endpoints support pagination with query parameters:

  • limit - Number of items per page (default: 20, max: 100)
  • offset - Number of items to skip (default: 0)
const response = await fetch(
  'https://runhuman.com/api/jobs?limit=50&offset=100&organizationId=org_abc',
  { headers: { 'Authorization': `Bearer ${API_KEY}` } }
);

const data = await response.json();
console.log('Total jobs:', data.pagination.total);
console.log('Has more:', data.pagination.hasMore);

Response format:

{
  "items": [],
  "pagination": {
    "total": 250,
    "limit": 50,
    "offset": 100,
    "hasMore": true
  }
}

Error Handling

All API errors follow a consistent format.

HTTP Status Codes

StatusMeaningAction
200SuccessRequest completed successfully
201CreatedResource created successfully
400Bad RequestCheck request parameters and format
401UnauthorizedVerify API key or PAT is valid and included
403ForbiddenInsufficient permissions for this resource
404Not FoundResource doesn’t exist or was deleted
408Request TimeoutSynchronous request exceeded 60 minutes
500Internal Server ErrorServer error, contact support if persistent

Error Response Format

{
  "error": "Error description",
  "message": "Detailed description"
}

Error Handling Best Practices

async function createJobWithRetry(url, description, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await fetch('https://runhuman.com/api/jobs', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.RUNHUMAN_API_KEY}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ url, description })
      });

      if (!response.ok) {
        const error = await response.json();

        // Don't retry client errors (4xx)
        if (response.status >= 400 && response.status < 500) {
          throw new Error(`API error: ${error.message}`);
        }

        // Retry server errors (5xx) with exponential backoff
        if (i < maxRetries - 1) {
          const delay = Math.pow(2, i) * 1000;
          await new Promise(resolve => setTimeout(resolve, delay));
          continue;
        }

        throw new Error(`API error after ${maxRetries} retries: ${error.message}`);
      }

      return await response.json();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
    }
  }
}

Next Steps

TopicLink
CLI tool for terminal usageCLI Documentation
Full technical specificationReference
Practical recipesCookbook
CI/CD integrationGitHub Actions