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"] }
ScopeAccess
fullUnrestricted (default)
test:createCreate, start, stop, pause, resume tests + read results + read quota
test:readRead-only access to tests and quota
readRead-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"
}
CodeHTTP StatusDescription
invalid_auth401Invalid or expired token/API key
insufficient_scope403API key lacks required scope
forbidden403Action not permitted (e.g. not an admin)
not_found404Resource does not exist or not in your org
validation_error400Invalid input (missing fields, bad format)
conflict409Duplicate resource (e.g. user already in org)
quota_exceeded403Monthly minute quota exhausted
worker_limit_exceeded403Requested workers exceed plan limit
duration_limit_exceeded403Test duration exceeds plan limit
region_restricted403Region not available on your plan
domain_blocked403Target domain is on the blocklist
invalid_script400Script failed validation or parsing
invalid_state400Invalid state transition (e.g. starting a non-ready test)
script_too_large400Script or asset exceeds size limit
limit_exceeded403Maximum templates or schedules per org reached
idempotency_error400Idempotency-Key too long or invalid
rate_limited429Too many requests
internal_error500Server 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

POST /api/v1/tests multipart/form-data scope: test:create
FieldTypeRequiredDescription
namestringyesTest name
scriptfileyesJavaScript test script (.js file)
workersintegeryesNumber of virtual users
durationintegeryesDuration in seconds
regionstringnoRegion 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

MethodEndpointScopeDescription
GET/api/v1/teststest:readList tests (paginated)
GET/api/v1/tests/:idtest:readGet test details + metrics + thresholds
POST/api/v1/tests/:id/starttest:createStart a ready test
POST/api/v1/tests/:id/stoptest:createStop a running test
POST/api/v1/tests/:id/pausetest:createPause a running test
POST/api/v1/tests/:id/resumetest:createResume a paused test
DELETE/api/v1/tests/:idfullDelete a test

Test Status Flow

provisioning -> ready -> pending -> running -> completed / failed
                  |                    |
                  v                    v
               cancelled            stopped

Test Comparison

Compare a test run against a baseline to detect performance regressions. Useful as a CI/CD gate.

GET /api/v1/tests/:id/compare/:baseline_id scope: test:read

# 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
fi

Webhooks

Receive HTTP callbacks when test lifecycle events occur. All deliveries include HMAC-SHA256 signatures for verification.

Events

EventFired When
test.completedTest finishes successfully
test.failedTest encounters a fatal error
test.startedTest begins running (workers executing)
test.readyWorkers provisioned, test ready to start
test.threshold_breachedA threshold condition was violated
test.scheduledA scheduled test was triggered

Endpoints

MethodEndpointDescription
POST/api/v1/webhooksCreate webhook endpoint
GET/api/v1/webhooksList webhook endpoints
PATCH/api/v1/webhooks/:idUpdate webhook (url, events, active)
DELETE/api/v1/webhooks/:idDelete webhook endpoint
GET/api/v1/webhooks/:id/deliveriesList recent deliveries
POST/api/v1/webhooks/:id/testSend 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.

MethodEndpointDescription
POST/api/v1/templatesCreate template
GET/api/v1/templatesList templates
GET/api/v1/templates/:idGet template details
PATCH/api/v1/templates/:idUpdate template
DELETE/api/v1/templates/:idDelete template
POST/api/v1/templates/:id/runRun 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.

MethodEndpointDescription
POST/api/v1/schedulesCreate schedule
GET/api/v1/schedulesList schedules
GET/api/v1/schedules/:idGet schedule details
PATCH/api/v1/schedules/:idUpdate schedule
DELETE/api/v1/schedules/:idDelete schedule
POST/api/v1/schedules/:id/pausePause schedule
POST/api/v1/schedules/:id/resumeResume paused schedule
POST/api/v1/schedules/:id/triggerTrigger 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 month

Analytics

Track performance trends across test runs over time.

Trends

GET /api/v1/analytics/trends scope: test:read
ParamDefaultDescription
metricp95p95, avg, error_rate, rps
period30d7d, 30d, 90d, up to 365d
target_urlallFilter 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

GET /api/v1/analytics/compare?test_ids=id1,id2,id3 scope: test:read

Compare up to 10 test runs side-by-side.

Other Endpoints

MethodEndpointDescription
GET/api/v1/meCurrent user profile
PATCH/api/v1/meUpdate profile
POST/api/v1/auth/change-passwordChange password
POST/api/v1/auth/logoutLogout
GET/api/v1/quotaCurrent quota usage
GET/api/v1/regionsAvailable regions for your tier
GET/api/v1/keysList API keys
POST/api/v1/keysCreate API key
DELETE/api/v1/keys/:idDelete API key
GET/api/v1/teamList team members
POST/api/v1/team/inviteInvite team member
GET/api/v1/team/invitesList pending invitations
DELETE/api/v1/team/invites/:idCancel invitation
POST/api/v1/team/invites/:id/resendResend invitation email
DELETE/api/v1/team/:user_idRemove team member
POST/api/v1/tests/validateValidate script syntax
GET/api/v1/tests/:id/workersActive workers for a test
GET/api/v1/tests/:id/queueQueue status for a test
GET/ws/runs/:test_idWebSocket for live metrics
GET/healthHealth check (public)
GET/health/deepDeep 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).