REST API
Integrate Runhuman directly from your backend, scripts, or any HTTP client.
Authentication
Include your API key in the Authorization header:
Authorization: Bearer YOUR_API_KEY
Get your key from the API Keys dashboard.
Synchronous Endpoint
POST /api/run creates a test and waits for completion. The request blocks until a human tester finishes (up to 10 minutes).
If the test does not complete within 10 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',
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/job/:jobId retrieves the job status and results.
async function pollJob(jobId) {
while (true) {
const response = await fetch(`https://runhuman.com/api/job/${jobId}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const job = await response.json();
if (job.status === 'completed') {
return job;
}
if (['incomplete', 'abandoned', '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
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
| url | string | Yes | - | URL for the tester to visit |
| description | string | Yes | - | Instructions for the tester |
| outputSchema | object | No | - | Schema defining data to extract. If omitted, only success/explanation returned |
| targetDurationMinutes | number | No | 5 | Time limit (1-60 minutes) |
| allowDurationExtension | boolean | No | true | Allow tester to request more time |
| maxExtensionMinutes | number/false | No | false | Maximum extension allowed |
| additionalValidationInstructions | string | No | - | Custom instructions for AI validation |
| screenSize | string/object | No | desktop | ”desktop”, “laptop”, “tablet”, “mobile”, or custom object |
| repoName | string | No | - | GitHub repo (“owner/repo”) for better AI context |
| canCreateGithubIssues | boolean | No | false | Auto-create GitHub issues from bugs. Requires repoName |
See Reference for full details on output schema format and response fields.
Handling Responses
A completed test returns:
{
"status": "completed",
"result": {
"success": true,
"explanation": "Login worked correctly. User was redirected to dashboard.",
"data": {
"loginWorks": true,
"redirectsToHome": true
}
},
"costUsd": 0.18,
"testDurationSeconds": 100,
"testerResponse": "I entered the credentials and clicked login...",
"testerAlias": "Alex",
"testerAvatarUrl": "https://images.subscribe.dev/uploads/.../phoenix.png",
"testerColor": "#FF6B35",
"testerData": {
"screenshots": ["https://..."],
"videoUrl": "https://..."
}
}
Tester Identity Fields:
testerAlias: Anonymized tester name (e.g., “Alex”, “Jordan”, “Sam”)testerAvatarUrl: Avatar image URL for displaying tester identitytesterColor: Hex color code for UI theming (e.g., “#FF6B35”)
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 GPT-4o 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 || 5
})
});
if (!response.ok) {
if (response.status === 408) {
throw new Error('Test timed out after 10 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?' }
}
);
Error Handling
| HTTP Status | Meaning |
|---|---|
| 400 | Invalid request parameters |
| 401 | Invalid or missing API key |
| 404 | Job not found |
| 408 | Synchronous request timed out (10 minutes) |
| 500 | Server error |
All errors return:
{
"error": "Error type",
"message": "Detailed description"
}
Projects API
Manage your projects and organize test jobs.
Create Project
POST /api/projects creates a new project.
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',
description: 'Production QA testing',
defaultUrl: 'https://myapp.com',
githubRepo: 'owner/repo'
})
});
const project = await response.json();
console.log('Project ID:', project.id);
List Projects
GET /api/projects returns all your projects.
const response = await fetch('https://runhuman.com/api/projects', {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const projects = 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
PUT /api/projects/:projectId updates project details.
const response = await fetch(`https://runhuman.com/api/projects/${projectId}`, {
method: 'PUT',
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 permanently 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.
List API Keys
GET /api/keys?projectId=proj_abc123 returns all keys for a project.
const response = await fetch(`https://runhuman.com/api/keys?projectId=${projectId}`, {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const keys = await response.json();
Create API Key
POST /api/keys creates a new 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({
projectId: 'proj_abc123',
name: 'Production Key',
description: 'Used in CI/CD pipeline'
})
});
const { key, keyId } = await response.json();
console.log('API Key:', key); // Only shown once!
Important: The full API key is only returned during creation. Store it securely.
Delete API Key
DELETE /api/keys/:keyId revokes an API key permanently.
const response = await fetch(`https://runhuman.com/api/keys/${keyId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
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 templates = 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: 5
})
}
);
const template = await response.json();
Get Template
GET /api/projects/:projectId/templates/:templateId retrieves template details.
Update Template
PUT /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.
Link Repository
POST /api/github/repos/:owner/:repo/link connects a GitHub repository to your project.
List Repositories
GET /api/github/repos returns all linked repositories.
const response = await fetch('https://runhuman.com/api/github/repos', {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const repos = await response.json();
List Issues
GET /api/github/issues/:owner/:repo retrieves issues from a repository.
const response = await fetch(
`https://runhuman.com/api/github/issues/myorg/myrepo?state=open&labels=bug`,
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
);
const issues = await response.json();
Query Parameters:
state- Filter by state (open/closed)labels- Filter by labels (comma-separated)assignee- Filter by assignee
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({
owner: 'myorg',
repo: 'myrepo',
issueNumber: 123,
url: 'https://preview-pr-123.myapp.com'
})
});
const job = await response.json();
Authentication & User
Get information about the current authenticated user.
Get Current User
GET /api/auth/me returns the authenticated user’s details (whoami).
const response = await fetch('https://runhuman.com/api/auth/me', {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const user = await response.json();
console.log('User ID:', user.id);
console.log('Email:', user.email);
Get Token Balance
GET /api/tokens/balance retrieves your current token/credit balance.
const response = await fetch('https://runhuman.com/api/tokens/balance', {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const { balance, currency } = await response.json();
console.log(`Balance: ${balance} ${currency}`);
Get Token History
GET /api/tokens/transactions returns your token usage history.
const response = await fetch('https://runhuman.com/api/tokens/transactions', {
headers: { 'Authorization': `Bearer ${API_KEY}` }
});
const transactions = await response.json();
Rate Limiting
API requests are rate-limited to ensure fair usage:
- Free tier: 100 requests/hour
- Pro tier: 1,000 requests/hour
- Enterprise: Custom limits
Rate limit information is returned in response headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 999
X-RateLimit-Reset: 1640000000
When rate limited, you’ll receive a 429 Too Many Requests response:
{
"error": "Rate limit exceeded",
"message": "Too many requests. Please try again in 3600 seconds.",
"retryAfter": 3600
}
Best practices:
- Cache responses when possible
- Implement exponential backoff for retries
- Use webhooks instead of polling when available
- Contact support for higher limits
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',
{ headers: { 'Authorization': `Bearer ${API_KEY}` } }
);
const data = await response.json();
console.log('Total jobs:', data.total);
console.log('Has more:', data.hasMore);
Response format:
{
"data": [...],
"total": 250,
"limit": 50,
"offset": 100,
"hasMore": true
}
Enhanced Error Handling
All API errors follow a consistent format with helpful details.
Error Response Format
{
"error": "ValidationError",
"message": "Detailed error description",
"statusCode": 400,
"details": {
"field": "url",
"reason": "Invalid URL format"
}
}
HTTP Status Codes
| Status | Meaning | Action |
|---|---|---|
| 200 | Success | Request completed successfully |
| 201 | Created | Resource created successfully |
| 400 | Bad Request | Check request parameters and format |
| 401 | Unauthorized | Verify API key is valid and included |
| 403 | Forbidden | Insufficient permissions for this resource |
| 404 | Not Found | Resource doesn’t exist or was deleted |
| 408 | Request Timeout | Synchronous request exceeded 10 minutes |
| 429 | Too Many Requests | Rate limit exceeded, check retry-after header |
| 500 | Internal Server Error | Server error, contact support if persistent |
| 503 | Service Unavailable | Temporary outage, retry with exponential backoff |
Common Error Scenarios
Invalid API Key:
{
"error": "AuthenticationError",
"message": "Invalid API key. Get your key from https://runhuman.com/dashboard/settings/api-keys",
"statusCode": 401
}
Resource Not Found:
{
"error": "NotFoundError",
"message": "Job job_abc123 not found",
"statusCode": 404
}
Validation Error:
{
"error": "ValidationError",
"message": "Invalid request parameters",
"statusCode": 400,
"details": {
"url": "URL is required",
"description": "Description must be at least 10 characters"
}
}
Rate Limit Exceeded:
{
"error": "RateLimitError",
"message": "Rate limit exceeded. Please try again in 1800 seconds.",
"statusCode": 429,
"retryAfter": 1800
}
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) and timeouts
if (i < maxRetries - 1) {
const delay = Math.pow(2, i) * 1000; // Exponential backoff
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
| Topic | Link |
|---|---|
| CLI tool for terminal usage | CLI Documentation |
| Full technical specification | Reference |
| Practical recipes | Cookbook |
| CI/CD integration | GitHub Actions |