REST API Reference
Programmatic access to Fusillade Cloud. Create tests, manage webhooks, automate schedules, and integrate into CI/CD pipelines.
Base URL https://api.fusillade.io
Content-Type application/json (unless noted)
Rate Limits 100 req/10s per IP (auth endpoints: 10 req/60s)
Authentication
All protected endpoints require either a Bearer token (JWT) or a scoped API key.
Bearer Token (JWT)
# Login
curl -X POST https://api.fusillade.io/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "you@example.com", "password": "..."}'
# Response: { "token": "eyJ...", "user": { ... } }Pass the token in subsequent requests:
curl https://api.fusillade.io/api/v1/me \
-H "Authorization: Bearer eyJ..."API Keys (Scoped)
API keys provide long-lived, scoped access. Create them from Settings or via the API.
# Create API key
curl -X POST https://api.fusillade.io/api/v1/keys \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "CI Agent", "scopes": ["test:create"]}'
# Response: { "key": "fusi_abc123...", "id": "...", "name": "CI Agent", "scopes": ["test:create"] }| Scope | Access |
|---|---|
| full | Unrestricted (default) |
| test:create | Create, start, stop, pause, resume tests + read results + read quota |
| test:read | Read-only access to tests and quota |
| read | Read-only access to everything |
Scopes are hierarchical: full > test:create > test:read > read.
Error Codes
All error responses include a machine-readable code field alongside the human-readable error message. Use code for programmatic error handling.
# Error response format
{
"error": "Worker limit exceeded. Your plan allows up to 1000 workers.",
"code": "worker_limit_exceeded"
}| Code | HTTP Status | Description |
|---|---|---|
| invalid_auth | 401 | Invalid or expired token/API key |
| insufficient_scope | 403 | API key lacks required scope |
| forbidden | 403 | Action not permitted (e.g. not an admin) |
| not_found | 404 | Resource does not exist or not in your org |
| validation_error | 400 | Invalid input (missing fields, bad format) |
| conflict | 409 | Duplicate resource (e.g. user already in org) |
| quota_exceeded | 403 | Monthly minute quota exhausted |
| worker_limit_exceeded | 403 | Requested workers exceed plan limit |
| duration_limit_exceeded | 403 | Test duration exceeds plan limit |
| region_restricted | 403 | Region not available on your plan |
| domain_blocked | 403 | Target domain is on the blocklist |
| invalid_script | 400 | Script failed validation or parsing |
| invalid_state | 400 | Invalid state transition (e.g. starting a non-ready test) |
| script_too_large | 400 | Script or asset exceeds size limit |
| limit_exceeded | 403 | Maximum templates or schedules per org reached |
| idempotency_error | 400 | Idempotency-Key too long or invalid |
| rate_limited | 429 | Too many requests |
| internal_error | 500 | Server error (retry with backoff) |
Idempotency
Prevent duplicate resource creation from retries by including an Idempotency-Key header on POST requests. Supported on test creation, webhook creation, and template creation.
# Idempotent request
curl -X POST https://api.fusillade.io/api/v1/tests \
-H "Authorization: Bearer <token>" \
-H "Idempotency-Key: my-unique-key-123" \
-F "name=Load Test" \
-F "script=@test.js" \
-F "workers=100" \
-F "duration=60"Keys are scoped to your organization and expire after 24 hours.
If a request with the same key is received within 24h, the original response is returned without creating a duplicate.
Use any unique string (UUID recommended). Maximum 256 characters.
Tests
Create Test
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | yes | Test name |
| script | file | yes | JavaScript test script (.js file) |
| workers | integer | yes | Number of virtual users |
| duration | integer | yes | Duration in seconds |
| region | string | no | Region code (default: eu-hel1) |
# Example
curl -X POST "https://api.fusillade.io/api/v1/tests" \
-H "Authorization: Bearer <token>" \
-F "name=API Load Test" \
-F "script=@test.js" \
-F "workers=100" \
-F "duration=60" \
-F "region=eu-hel1"
# Response (201):
{
"test_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "provisioning"
}Wait for Ready (?wait=true)
Add ?wait=true to long-poll until the test reaches ready status. Useful for CI/CD pipelines that need to start tests immediately after creation.
# Wait for provisioning
# Blocks until workers are provisioned (up to 5 min timeout)
curl -X POST "https://api.fusillade.io/api/v1/tests?wait=true" \
-H "Authorization: Bearer <token>" \
-F "name=CI Test" \
-F "script=@test.js" \
-F "workers=50" \
-F "duration=60"
# Response (201) -- returned once status is "ready":
{
"test_id": "550e8400-...",
"status": "ready"
}
# Wait for full completion (timeout: 30 min)
curl -X POST "https://api.fusillade.io/api/v1/tests?wait=true&wait_for=completed" \
-H "Authorization: Bearer <token>" \
-F "name=CI Test" \
-F "script=@test.js" \
-F "workers=50" \
-F "duration=60"?wait=true -- polls every 2s, returns when status is ready (timeout: 5 min)
?wait=true&wait_for=completed -- waits for completed status (timeout: 30 min)
If the timeout is reached, the current status is returned (the test is not cancelled).
Other Test Endpoints
| Method | Endpoint | Scope | Description |
|---|---|---|---|
| GET | /api/v1/tests | test:read | List tests (paginated) |
| GET | /api/v1/tests/:id | test:read | Get test details + metrics + thresholds |
| POST | /api/v1/tests/:id/start | test:create | Start a ready test |
| POST | /api/v1/tests/:id/stop | test:create | Stop a running test |
| POST | /api/v1/tests/:id/pause | test:create | Pause a running test |
| POST | /api/v1/tests/:id/resume | test:create | Resume a paused test |
| DELETE | /api/v1/tests/:id | full | Delete a test |
Test Status Flow
provisioning -> ready -> pending -> running -> completed / failed
| |
v v
cancelled stoppedTest Comparison
Compare a test run against a baseline to detect performance regressions. Useful as a CI/CD gate.
# Compare against baseline
curl "https://api.fusillade.io/api/v1/tests/<current_id>/compare/<baseline_id>" \
-H "Authorization: Bearer <token>"
# Response:
{
"baseline": {
"name": "API Load Test v1.2",
"status": "completed",
"workers": 100,
"duration_seconds": 60,
"region": "eu-hel1",
"avg_latency_ms": 42.5,
"p95_latency_ms": 89.2,
"error_rate": 0.001,
"rps": 1250.0,
"total_requests": 75000
},
"current": {
"name": "API Load Test v1.3",
"status": "completed",
"workers": 100,
"duration_seconds": 60,
"region": "eu-hel1",
"avg_latency_ms": 48.1,
"p95_latency_ms": 102.4,
"error_rate": 0.002,
"rps": 1180.0,
"total_requests": 70800
},
"delta": {
"avg_latency_ms": 5.6,
"p95_latency_ms": 13.2,
"error_rate": 0.001,
"rps": -70.0
},
"regression": true,
"regression_reasons": [
"p95 latency increased 14.8%"
]
}Regression detection rules:
A test is flagged as a regression if p95 latency increased more than 10% or error rate increased more than 0.5 percentage points.
CI/CD Example
# GitHub Actions snippet
# Run test, wait for completion, check for regression
TEST_ID=$(curl -s -X POST "https://api.fusillade.io/api/v1/tests?wait=true&wait_for=completed" \
-H "Authorization: Bearer $FUSILLADE_API_KEY" \
-F "name=CI Run #$GITHUB_RUN_NUMBER" \
-F "script=@loadtest.js" \
-F "workers=50" \
-F "duration=60" | jq -r '.test_id')
REGRESSION=$(curl -s "https://api.fusillade.io/api/v1/tests/$TEST_ID/compare/$BASELINE_ID" \
-H "Authorization: Bearer $FUSILLADE_API_KEY" | jq -r '.regression')
if [ "$REGRESSION" = "true" ]; then
echo "Performance regression detected"
exit 1
fiWebhooks
Receive HTTP callbacks when test lifecycle events occur. All deliveries include HMAC-SHA256 signatures for verification.
Events
| Event | Fired When |
|---|---|
| test.completed | Test finishes successfully |
| test.failed | Test encounters a fatal error |
| test.started | Test begins running (workers executing) |
| test.ready | Workers provisioned, test ready to start |
| test.threshold_breached | A threshold condition was violated |
| test.scheduled | A scheduled test was triggered |
Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/webhooks | Create webhook endpoint |
| GET | /api/v1/webhooks | List webhook endpoints |
| PATCH | /api/v1/webhooks/:id | Update webhook (url, events, active) |
| DELETE | /api/v1/webhooks/:id | Delete webhook endpoint |
| GET | /api/v1/webhooks/:id/deliveries | List recent deliveries |
| POST | /api/v1/webhooks/:id/test | Send test delivery |
# Create webhook
curl -X POST https://api.fusillade.io/api/v1/webhooks \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-server.com/webhook",
"events": ["test.completed", "test.failed", "test.ready"]
}'
# Response (201):
{
"id": "...",
"url": "https://your-server.com/webhook",
"events": ["test.completed", "test.failed", "test.ready"],
"secret": "whsec_...",
"active": true,
"created_at": "2026-02-06T12:00:00Z"
}Payload Format
# Webhook delivery
{
"event": "test.completed",
"timestamp": "2026-02-06T12:00:00Z",
"data": {
"test_id": "550e8400-...",
"name": "API Load Test",
"status": "completed",
"workers": 100,
"duration_seconds": 60,
"p95_latency_ms": 42,
"error_rate": 0.001,
"target_urls": ["https://api.example.com"],
"threshold_results": null
}
}Signature Verification
Every delivery includes an X-Fusillade-Signature header for HMAC-SHA256 verification.
# Verify signature (Node.js)
const crypto = require('crypto');
function verifyWebhook(body, signature, secret) {
const [tPart, vPart] = signature.split(',');
const timestamp = tPart.replace('t=', '');
const expectedSig = vPart.replace('v1=', '');
const payload = timestamp + '.' + body;
const computed = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(expectedSig)
);
}Retry policy: Failed deliveries retry with exponential backoff (30s, 120s, 480s). Max 3 attempts.
Security: Webhook URLs must be HTTPS. Private IPs, localhost, and metadata endpoints are blocked (SSRF prevention).
Templates
Save test configurations as reusable templates. Templates store the script, worker count, duration, and region so you can re-run common tests without re-uploading.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/templates | Create template |
| GET | /api/v1/templates | List templates |
| GET | /api/v1/templates/:id | Get template details |
| PATCH | /api/v1/templates/:id | Update template |
| DELETE | /api/v1/templates/:id | Delete template |
| POST | /api/v1/templates/:id/run | Run test from template (with optional overrides) |
# Create template
curl -X POST https://api.fusillade.io/api/v1/templates \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "API Smoke Test",
"description": "Quick smoke test for the main API",
"script_content": "export const options = { workers: 10, duration: "30s" };\nexport default function() { http.get("https://api.example.com/health"); }",
"workers": 10,
"duration_seconds": 30,
"region": "eu-hel1"
}'# Run template with overrides
curl -X POST https://api.fusillade.io/api/v1/templates/<template_id>/run \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"workers": 50, "duration_seconds": 120}'
# Response: same as POST /api/v1/tests
{
"test_id": "...",
"status": "provisioning"
}Schedules
Automate recurring tests with cron expressions. Each schedule contains its own script and configuration.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/v1/schedules | Create schedule |
| GET | /api/v1/schedules | List schedules |
| GET | /api/v1/schedules/:id | Get schedule details |
| PATCH | /api/v1/schedules/:id | Update schedule |
| DELETE | /api/v1/schedules/:id | Delete schedule |
| POST | /api/v1/schedules/:id/pause | Pause schedule |
| POST | /api/v1/schedules/:id/resume | Resume paused schedule |
| POST | /api/v1/schedules/:id/trigger | Trigger immediate execution |
# Create schedule
curl -X POST https://api.fusillade.io/api/v1/schedules \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{
"name": "Nightly API Test",
"script_content": "export default function() { http.get("https://api.example.com/health"); }",
"workers": 50,
"duration_seconds": 60,
"region": "eu-hel1",
"cron_expression": "0 0 2 * * * *",
"timezone": "UTC"
}'
# Cron format: sec min hour day_of_month month day_of_week year (7 fields)
# "0 0 2 * * * *" -- daily at 2:00 AM
# "0 0 */6 * * * *" -- every 6 hours
# "0 0 9 * * Mon *" -- every Monday at 9:00 AM
# "0 0 0 1 * * *" -- first day of each monthAnalytics
Track performance trends across test runs over time.
Trends
| Param | Default | Description |
|---|---|---|
| metric | p95 | p95, avg, error_rate, rps |
| period | 30d | 7d, 30d, 90d, up to 365d |
| target_url | all | Filter by target URL |
# Get trends
curl "https://api.fusillade.io/api/v1/analytics/trends?metric=p95&period=30d" \
-H "Authorization: Bearer <token>"
curl "https://api.fusillade.io/api/v1/analytics/trends?target_url=https://api.example.com&metric=error_rate&period=7d" \
-H "Authorization: Bearer <token>"Side-by-Side Compare
Compare up to 10 test runs side-by-side.
Other Endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/me | Current user profile |
| PATCH | /api/v1/me | Update profile |
| POST | /api/v1/auth/change-password | Change password |
| POST | /api/v1/auth/logout | Logout |
| GET | /api/v1/quota | Current quota usage |
| GET | /api/v1/regions | Available regions for your tier |
| GET | /api/v1/keys | List API keys |
| POST | /api/v1/keys | Create API key |
| DELETE | /api/v1/keys/:id | Delete API key |
| GET | /api/v1/team | List team members |
| POST | /api/v1/team/invite | Invite team member |
| GET | /api/v1/team/invites | List pending invitations |
| DELETE | /api/v1/team/invites/:id | Cancel invitation |
| POST | /api/v1/team/invites/:id/resend | Resend invitation email |
| DELETE | /api/v1/team/:user_id | Remove team member |
| POST | /api/v1/tests/validate | Validate script syntax |
| GET | /api/v1/tests/:id/workers | Active workers for a test |
| GET | /api/v1/tests/:id/queue | Queue status for a test |
| GET | /ws/runs/:test_id | WebSocket for live metrics |
| GET | /health | Health check (public) |
| GET | /health/deep | Deep health check with DB/Redis (public) |
Live Metrics (WebSocket)
Stream real-time metrics from a running test via WebSocket.
# Connect (Browser)
// Pass token via Sec-WebSocket-Protocol header as ['auth', '<token>']
const ws = new WebSocket(
'wss://api.fusillade.io/ws/runs/<test_id>',
['auth', '<your_jwt_token>']
);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// data.type: "metric" | "status" | "summary"
console.log(data);
};# Connect (CLI / server-side)
// Query parameter fallback for non-browser clients
// wss://api.fusillade.io/ws/runs/<test_id>?token=<jwt_token>Authentication: Pass JWT via Sec-WebSocket-Protocol header as auth, <token>. In browsers, use ['auth', token] as the second argument to new WebSocket(). A query parameter fallback (?token=) is also supported.
Message types: metric (periodic stats), status (state changes), summary (final results).