REST API Reference
Complete reference for the Runhooks REST API. Schedule, manage, and monitor HTTP webhooks programmatically.
https://api.runhooks.app/api/v1Authentication
Most endpoints require authentication via an API key passed in the Authorization header. API keys are prefixed with rh_live_.
Authorization: Bearer rh_live_xxxxxxxxxxxxHow to get an API key:
- Dashboard — Register at runhooks.app/register, then copy your key from Settings.
- API — Call POST /auth/register-anonymous to get an instant key with no email required.
- CLI — Run
runhooks initto create an account and get a key.
Web sessions also support JWT access tokens (returned by /auth/login and /auth/register). JWT tokens expire after 15 minutes and can be refreshed via /auth/refresh.
Quick Start
Go from zero to a running scheduled webhook in 4 steps:
+-----------+ +-----------+ +-----------+ +--------------+
| Register |---->| Create |---->| List |---->| View |
| Account | | Job | | Jobs | | Executions |
+-----------+ +-----------+ +-----------+ +--------------+
Get API Key Set schedule & Filter & page Check status &
HTTP config response data1. Create an account
const res = await fetch("https://api.runhooks.app/api/v1/auth/register-anonymous", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "My Server" }),
});
const { data } = await res.json();
const apiKey = data.apiKey; // Save this!Save the apiKey from the response — it cannot be retrieved again.
2. Create a job
const res = await fetch("https://api.runhooks.app/api/v1/jobs", {
method: "POST",
headers: {
"Authorization": "Bearer rh_live_xxxxxxxxxxxx",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Sync inventory",
schedule: { type: "cron", expression: "*/5 * * * *" },
httpConfig: {
url: "https://api.example.com/sync",
method: "POST",
},
}),
});
const { data: job } = await res.json();3. List your jobs
const res = await fetch("https://api.runhooks.app/api/v1/jobs", {
headers: { "Authorization": "Bearer rh_live_xxxxxxxxxxxx" },
});
const { data } = await res.json();4. View executions
const res = await fetch(`https://api.runhooks.app/api/v1/jobs/${jobId}/executions`, {
headers: { "Authorization": "Bearer rh_live_xxxxxxxxxxxx" },
});
const { data } = await res.json();Response Format
Every response is a JSON object with a consistent wrapper:
{
"success": true,
"data": { ... },
"message": "Optional human-readable message"
}Error responses follow the same shape:
{
"success": false,
"error": "Description of what went wrong"
}Paginated endpoints return data as:
{
"success": true,
"data": {
"data": [ ... ],
"total": 42,
"page": 1,
"limit": 20,
"totalPages": 3
}
}Error status codes
| Status | Meaning |
|---|---|
400 | Bad request — invalid body, missing required fields, or validation failure |
401 | Unauthorized — missing or invalid API key / token |
403 | Forbidden — insufficient plan or permissions |
404 | Not found — resource does not exist or does not belong to you |
409 | Conflict — resource already exists (e.g. duplicate email) |
429 | Too many requests — rate limit exceeded |
500 | Internal server error |
503 | Service unavailable — a backing service (database, Redis) is down |
Rate Limits
Rate limits are enforced per IP or per user depending on the endpoint. When a limit is exceeded the API responds with 429 and standard draft-7 rate-limit headers.
| Scope | Limit | Window | Key |
|---|---|---|---|
| Global (all routes) | 100 requests | 15 minutes | IP |
| Job writes (create/update/delete) | 30 requests | 1 minute | User ID |
| Execution replay | 10 requests | 1 minute | User ID |
| Registration | 5 requests | 1 hour | IP |
| Login / refresh | 10 requests | 1 minute | IP |
Jobs
Jobs are scheduled HTTP requests. Each job has a schedule (cron or interval), an HTTP config (URL, method, headers, body), and a retry policy.
POST /jobs
Create a new scheduled job.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | required | — | Job name (1–255 chars) |
schedule.type | string | required | — | "cron" or "interval" |
schedule.expression | string | required | — | Cron expression (e.g. "*/5 * * * *") or interval in ms (e.g. "60000") |
httpConfig.url | string | required | — | Target URL (must be valid, no private IPs) |
description | string | optional | — | Human-readable description (max 2000 chars) |
schedule.timezone | string | optional | — | IANA timezone (cron only) |
httpConfig.method | string | optional | "GET" | GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS |
httpConfig.headers | object | optional | — | Key-value pairs of HTTP headers |
httpConfig.body | string | optional | — | Request body (max 1 MB) |
httpConfig.timeoutMs | number | optional | 30000 | Request timeout in ms (1,000–300,000, capped by plan) |
retryPolicy.maxRetries | number | optional | 3 | Max retry attempts (0–10, capped by plan) |
retryPolicy.initialDelay | number | optional | 1000 | Initial retry delay in ms (100–60,000) |
retryPolicy.backoffMultiplier | number | optional | 2 | Backoff multiplier (1–10) |
retryPolicy.maxDelay | number | optional | 300000 | Max retry delay in ms (1,000–3,600,000) |
tags | string[] | optional | — | Tags (max 20 items, each max 50 chars) |
const res = await fetch("https://api.runhooks.app/api/v1/jobs", {
method: "POST",
headers: {
"Authorization": "Bearer rh_live_xxxxxxxxxxxx",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "Refresh cache",
schedule: { type: "cron", expression: "0 * * * *", timezone: "America/New_York" },
httpConfig: {
url: "https://api.example.com/refresh",
method: "POST",
headers: { "X-Api-Key": "secret" },
},
retryPolicy: { maxRetries: 5 },
tags: ["prod", "cache"],
}),
});
const { data: job } = await res.json();Response (201):
{
"success": true,
"message": "Job created successfully",
"data": {
"id": "665a1b2c3d4e5f6a7b8c9d0e",
"name": "Refresh cache",
"schedule": { "type": "cron", "expression": "0 * * * *", "timezone": "America/New_York" },
"httpConfig": {
"url": "https://api.example.com/refresh",
"method": "POST",
"headers": { "X-Api-Key": "secret" },
"timeoutMs": 30000
},
"retryPolicy": { "maxRetries": 5, "initialDelay": 1000, "backoffMultiplier": 2, "maxDelay": 300000 },
"status": "active",
"tags": ["prod", "cache"],
"createdAt": "2025-01-15T12:00:00.000Z",
"updatedAt": "2025-01-15T12:00:00.000Z",
"nextExecutionAt": "2025-01-15T13:00:00.000Z"
}
}GET /jobs
List your jobs with optional filters and pagination.
| Query param | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number (min 1) |
limit | number | 20 | Results per page (1–100) |
status | string | — | Filter by status: active, paused, completed, failed |
tag | string | — | Filter by tag |
name | string | — | Filter by name (case-insensitive regex, max 100 chars) |
const res = await fetch("https://api.runhooks.app/api/v1/jobs?status=active&limit=10", {
headers: { "Authorization": "Bearer rh_live_xxxxxxxxxxxx" },
});
const { data } = await res.json();
console.log(data.data); // Job[]
console.log(data.totalPages); // numberGET /jobs/:id
Get a single job by ID.
const res = await fetch(`https://api.runhooks.app/api/v1/jobs/${jobId}`, {
headers: { "Authorization": "Bearer rh_live_xxxxxxxxxxxx" },
});
const { data: job } = await res.json();PATCH /jobs/:id
Update a job. Only include the fields you want to change. Also available as PUT.
| Field | Type | Description |
|---|---|---|
name | string | Job name (1–255 chars) |
description | string | Description (max 2000 chars) |
schedule | object | Schedule object (type, expression, timezone) |
httpConfig | object | HTTP config object (url, method, headers, body, timeoutMs) |
retryPolicy | object | Retry policy object |
status | string | Set to "active" or "paused" |
tags | string[] | Replace all tags |
const res = await fetch(`https://api.runhooks.app/api/v1/jobs/${jobId}`, {
method: "PATCH",
headers: {
"Authorization": "Bearer rh_live_xxxxxxxxxxxx",
"Content-Type": "application/json",
},
body: JSON.stringify({ name: "New name", status: "paused" }),
});
const { data: job } = await res.json();DELETE /jobs/:id
Delete a job permanently.
const res = await fetch(`https://api.runhooks.app/api/v1/jobs/${jobId}`, {
method: "DELETE",
headers: { "Authorization": "Bearer rh_live_xxxxxxxxxxxx" },
});
const { success } = await res.json();GET /jobs/:id/executions
List executions for a specific job.
| Query param | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number |
limit | number | 20 | Results per page (1–100) |
status | string | — | Filter: pending, running, success, failed, timeout, dead_letter, quota_exceeded |
const res = await fetch(
`https://api.runhooks.app/api/v1/jobs/${jobId}/executions?status=failed`,
{ headers: { "Authorization": "Bearer rh_live_xxxxxxxxxxxx" } },
);
const { data } = await res.json();Response (200):
{
"success": true,
"data": {
"data": [
{
"id": "665b2c3d4e5f6a7b8c9d0e1f",
"jobId": "665a1b2c3d4e5f6a7b8c9d0e",
"status": "success",
"startedAt": "2025-01-15T13:00:00.000Z",
"completedAt": "2025-01-15T13:00:01.234Z",
"durationMs": 1234,
"httpStatusCode": 200,
"responseBody": "{\"ok\":true}",
"attempt": 1
}
],
"total": 1,
"page": 1,
"limit": 20,
"totalPages": 1
}
}Executions
Executions represent individual runs of a job. Use these endpoints to list executions across all jobs or replay a failed execution.
GET /executions
List executions across all your jobs.
| Query param | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number |
limit | number | 20 | Results per page (1–100) |
status | string | — | Filter by status |
jobId | string | — | Filter by job ID (must be a job you own) |
const res = await fetch("https://api.runhooks.app/api/v1/executions?limit=5", {
headers: { "Authorization": "Bearer rh_live_xxxxxxxxxxxx" },
});
const { data } = await res.json();POST /executions/:id/replay
Re-run a past execution immediately. Useful for recovering from transient failures. The replay creates a new execution linked to the original.
const res = await fetch(
`https://api.runhooks.app/api/v1/executions/${executionId}/replay`,
{
method: "POST",
headers: { "Authorization": "Bearer rh_live_xxxxxxxxxxxx" },
},
);
const { message } = await res.json(); // "Execution replay enqueued"Response (202):
{
"success": true,
"message": "Execution replay enqueued"
}Alerts
Alerts notify you when jobs fail. Supports email, webhook, and Slack channels.
POST /alerts
Create a new alert configuration.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | required | — | Alert name (1–100 chars) |
channel | string | required | — | "email", "webhook", or "slack" |
target | string | required | — | Email address, webhook URL, or Slack URL (max 500 chars) |
jobId | string|null | optional | — | Scope to a specific job (omit for all jobs) |
consecutiveFailuresThreshold | number | optional | 1 | Consecutive failures before alert fires (1–100) |
cooldownMinutes | number | optional | 60 | Minutes between repeat alerts (0–10,080) |
enabled | boolean | optional | true | Whether the alert is active |
const res = await fetch("https://api.runhooks.app/api/v1/alerts", {
method: "POST",
headers: {
"Authorization": "Bearer rh_live_xxxxxxxxxxxx",
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "All failures",
channel: "email",
target: "[email protected]",
consecutiveFailuresThreshold: 3,
cooldownMinutes: 30,
}),
});
const { data: alert } = await res.json();Response (201):
{
"success": true,
"message": "Alert config created",
"data": {
"id": "665c3d4e5f6a7b8c9d0e1f2a",
"userId": "665a0a1b2c3d4e5f6a7b8c9d",
"name": "All failures",
"channel": "email",
"target": "[email protected]",
"consecutiveFailuresThreshold": 3,
"cooldownMinutes": 30,
"enabled": true,
"createdAt": "2025-01-15T12:00:00.000Z",
"updatedAt": "2025-01-15T12:00:00.000Z"
}
}GET /alerts
List all your alert configurations. Returns a flat array (not paginated).
const res = await fetch("https://api.runhooks.app/api/v1/alerts", {
headers: { "Authorization": "Bearer rh_live_xxxxxxxxxxxx" },
});
const { data: alerts } = await res.json(); // AlertConfig[]GET /alerts/:id
Get a single alert configuration by ID.
const res = await fetch(`https://api.runhooks.app/api/v1/alerts/${alertId}`, {
headers: { "Authorization": "Bearer rh_live_xxxxxxxxxxxx" },
});
const { data: alert } = await res.json();PUT /alerts/:id
Update an alert configuration. Only include the fields you want to change.
| Field | Type | Description |
|---|---|---|
name | string | Alert name (1–100 chars) |
channel | string | email, webhook, or slack |
target | string | Destination (max 500 chars) |
jobId | string|null | Scope to job or null for all |
consecutiveFailuresThreshold | number | 1–100 |
cooldownMinutes | number | 0–10,080 |
enabled | boolean | Enable or disable |
const res = await fetch(`https://api.runhooks.app/api/v1/alerts/${alertId}`, {
method: "PUT",
headers: {
"Authorization": "Bearer rh_live_xxxxxxxxxxxx",
"Content-Type": "application/json",
},
body: JSON.stringify({ enabled: false }),
});
const { data: alert } = await res.json();DELETE /alerts/:id
Delete an alert configuration.
const res = await fetch(`https://api.runhooks.app/api/v1/alerts/${alertId}`, {
method: "DELETE",
headers: { "Authorization": "Bearer rh_live_xxxxxxxxxxxx" },
});
const { message } = await res.json();Account
POST /auth/register
Create a full account with email and password. Returns JWT tokens and an API key.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | required | Valid email address |
password | string | required | 8–128 characters |
name | string | required | Display name (1–255 chars) |
const res = await fetch("https://api.runhooks.app/api/v1/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: "[email protected]",
password: "securepass",
name: "My Name",
}),
});
const { data } = await res.json();
// data.user, data.tokens, data.apiKeyResponse (201):
{
"success": true,
"message": "Registration successful. Save the API key -- it cannot be retrieved again.",
"data": {
"user": { "id": "...", "email": "[email protected]", "name": "My Name", "plan": "free", "role": "user" },
"tokens": { "accessToken": "eyJ...", "refreshToken": "eyJ..." },
"apiKey": "rh_live_xxxxxxxxxxxx"
}
}POST /auth/register-anonymous
Create an anonymous account with no email or password. Returns an API key only (no JWT tokens).
| Field | Type | Required | Description |
|---|---|---|---|
name | string | optional | Display name (1–255 chars) |
Response (201):
{
"success": true,
"message": "Anonymous account created. Save the API key -- it cannot be retrieved again.",
"data": {
"user": { "id": "...", "name": "My Server", "plan": "free", "role": "user", "isAnonymous": true },
"apiKey": "rh_live_xxxxxxxxxxxx"
}
}POST /auth/login
Sign in with email and password. Returns JWT tokens for web session use.
| Field | Type | Required | Description |
|---|---|---|---|
email | string | required | Valid email address |
password | string | required | Account password |
const res = await fetch("https://api.runhooks.app/api/v1/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: "[email protected]", password: "securepass" }),
});
const { data } = await res.json();
// data.user, data.tokens.accessToken, data.tokens.refreshTokenPOST /auth/refresh
Exchange a refresh token for new access and refresh tokens. Access tokens expire after 15 minutes; refresh tokens last 30 days.
| Field | Type | Required | Description |
|---|---|---|---|
refreshToken | string | required | A valid refresh token |
const res = await fetch("https://api.runhooks.app/api/v1/auth/refresh", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ refreshToken: "eyJ..." }),
});
const { data: tokens } = await res.json();
// tokens.accessToken, tokens.refreshTokenGET /auth/me
Get the authenticated user's profile.
const res = await fetch("https://api.runhooks.app/api/v1/auth/me", {
headers: { "Authorization": "Bearer rh_live_xxxxxxxxxxxx" },
});
const { data: user } = await res.json();Response (200):
{
"success": true,
"data": {
"id": "665a0a1b2c3d4e5f6a7b8c9d",
"email": "[email protected]",
"name": "My Name",
"role": "user",
"plan": "free",
"isAnonymous": false,
"apiKeyPrefix": "rh_live_abc1",
"createdAt": "2025-01-15T12:00:00.000Z",
"updatedAt": "2025-01-15T12:00:00.000Z"
}
}POST /auth/rotate-key
Generate a new API key. The old key stops working immediately.
const res = await fetch("https://api.runhooks.app/api/v1/auth/rotate-key", {
method: "POST",
headers: { "Authorization": "Bearer rh_live_xxxxxxxxxxxx" },
});
const { data } = await res.json();
// data.apiKey (new key), data.apiKeyPrefixResponse (200):
{
"success": true,
"message": "API key rotated. Save the new key -- it cannot be retrieved again.",
"data": {
"apiKey": "rh_live_yyyyyyyyyyyy",
"apiKeyPrefix": "rh_live_yyyy"
}
}Usage & Limits
GET /usage/me
Get your current plan, limits, and usage.
const res = await fetch("https://api.runhooks.app/api/v1/usage/me", {
headers: { "Authorization": "Bearer rh_live_xxxxxxxxxxxx" },
});
const { data: usage } = await res.json();Response (200):
{
"success": true,
"data": {
"plan": "free",
"limits": {
"maxJobs": 3,
"maxDailyRuns": 200,
"maxMonthlyRuns": 2000,
"maxRetries": 3,
"maxRequestTimeoutMs": 30000,
"maxAlertConfigs": 1,
"retentionDays": 1,
"concurrency": 1
},
"usage": {
"jobs": { "used": 1, "limit": 3, "unlimited": false },
"dailyRuns": { "used": 42, "limit": 200, "unlimited": false, "resetsAt": "2025-01-16T00:00:00.000Z" },
"monthlyRuns": { "used": 350, "limit": 2000, "unlimited": false, "resetsAt": "2025-02-01T00:00:00.000Z" },
"alertConfigs": { "used": 0, "limit": 1, "unlimited": false }
}
}
}Plan limits comparison
| Limit | Free | Starter | Production | Growth |
|---|---|---|---|---|
| Max jobs | 3 | 10 | 30 | 100 |
| Daily runs | 200 | 2,000 | 12,000 | 50,000 |
| Monthly runs | 2,000 | 20,000 | 120,000 | 500,000 |
| Max retries | 3 | 5 | 7 | 10 |
| Request timeout | 30s | 60s | 120s | 300s |
| Alert configs | 1 | 3 | 10 | 50 |
| Log retention | 1 day | 3 days | 14 days | 30 days |
| Concurrency | 1 | 2 | 5 | 15 |
Health Check
GET /health
Public endpoint (no authentication required). Returns the API status and backing service health.
const res = await fetch("https://api.runhooks.app/api/v1/health");
const { data } = await res.json();Response (200 when healthy, 503 when degraded):
{
"success": true,
"data": {
"status": "ok",
"version": "0.1.0",
"uptime": 86400,
"timestamp": "2025-01-15T12:00:00.000Z",
"services": {
"mongodb": { "status": "ok" },
"redis": { "status": "ok", "latencyMs": 2 }
}
}
}