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"
}
Next Steps
| Topic | Link |
|---|---|
| Full technical specification | Reference |
| Practical recipes | Cookbook |
| CI/CD integration | GitHub Actions |