REST API Reference

Complete reference for the Runhooks REST API. Schedule, manage, and monitor HTTP webhooks programmatically.

Base URL
https://api.runhooks.app/api/v1

Authentication

Most endpoints require authentication via an API key passed in the Authorization header. API keys are prefixed with rh_live_.

header
Authorization: Bearer rh_live_xxxxxxxxxxxx

How to get an API 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 data

1. 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:

json
{
  "success": true,
  "data": { ... },
  "message": "Optional human-readable message"
}

Error responses follow the same shape:

json
{
  "success": false,
  "error": "Description of what went wrong"
}

Paginated endpoints return data as:

json
{
  "success": true,
  "data": {
    "data": [ ... ],
    "total": 42,
    "page": 1,
    "limit": 20,
    "totalPages": 3
  }
}

Error status codes

StatusMeaning
400Bad request — invalid body, missing required fields, or validation failure
401Unauthorized — missing or invalid API key / token
403Forbidden — insufficient plan or permissions
404Not found — resource does not exist or does not belong to you
409Conflict — resource already exists (e.g. duplicate email)
429Too many requests — rate limit exceeded
500Internal server error
503Service 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.

ScopeLimitWindowKey
Global (all routes)100 requests15 minutesIP
Job writes (create/update/delete)30 requests1 minuteUser ID
Execution replay10 requests1 minuteUser ID
Registration5 requests1 hourIP
Login / refresh10 requests1 minuteIP

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.

FieldTypeRequiredDefaultDescription
namestringrequiredJob name (1–255 chars)
schedule.typestringrequired"cron" or "interval"
schedule.expressionstringrequiredCron expression (e.g. "*/5 * * * *") or interval in ms (e.g. "60000")
httpConfig.urlstringrequiredTarget URL (must be valid, no private IPs)
descriptionstringoptionalHuman-readable description (max 2000 chars)
schedule.timezonestringoptionalIANA timezone (cron only)
httpConfig.methodstringoptional"GET"GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS
httpConfig.headersobjectoptionalKey-value pairs of HTTP headers
httpConfig.bodystringoptionalRequest body (max 1 MB)
httpConfig.timeoutMsnumberoptional30000Request timeout in ms (1,000–300,000, capped by plan)
retryPolicy.maxRetriesnumberoptional3Max retry attempts (0–10, capped by plan)
retryPolicy.initialDelaynumberoptional1000Initial retry delay in ms (100–60,000)
retryPolicy.backoffMultipliernumberoptional2Backoff multiplier (1–10)
retryPolicy.maxDelaynumberoptional300000Max retry delay in ms (1,000–3,600,000)
tagsstring[]optionalTags (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):

json
{
  "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 paramTypeDefaultDescription
pagenumber1Page number (min 1)
limitnumber20Results per page (1–100)
statusstringFilter by status: active, paused, completed, failed
tagstringFilter by tag
namestringFilter 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);  // number

GET /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.

FieldTypeDescription
namestringJob name (1–255 chars)
descriptionstringDescription (max 2000 chars)
scheduleobjectSchedule object (type, expression, timezone)
httpConfigobjectHTTP config object (url, method, headers, body, timeoutMs)
retryPolicyobjectRetry policy object
statusstringSet to "active" or "paused"
tagsstring[]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 paramTypeDefaultDescription
pagenumber1Page number
limitnumber20Results per page (1–100)
statusstringFilter: 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):

json
{
  "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 paramTypeDefaultDescription
pagenumber1Page number
limitnumber20Results per page (1–100)
statusstringFilter by status
jobIdstringFilter 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):

json
{
  "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.

FieldTypeRequiredDefaultDescription
namestringrequiredAlert name (1–100 chars)
channelstringrequired"email", "webhook", or "slack"
targetstringrequiredEmail address, webhook URL, or Slack URL (max 500 chars)
jobIdstring|nulloptionalScope to a specific job (omit for all jobs)
consecutiveFailuresThresholdnumberoptional1Consecutive failures before alert fires (1–100)
cooldownMinutesnumberoptional60Minutes between repeat alerts (0–10,080)
enabledbooleanoptionaltrueWhether 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):

json
{
  "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.

FieldTypeDescription
namestringAlert name (1–100 chars)
channelstringemail, webhook, or slack
targetstringDestination (max 500 chars)
jobIdstring|nullScope to job or null for all
consecutiveFailuresThresholdnumber1–100
cooldownMinutesnumber0–10,080
enabledbooleanEnable 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.

FieldTypeRequiredDescription
emailstringrequiredValid email address
passwordstringrequired8–128 characters
namestringrequiredDisplay 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.apiKey

Response (201):

json
{
  "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).

FieldTypeRequiredDescription
namestringoptionalDisplay name (1–255 chars)

Response (201):

json
{
  "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.

FieldTypeRequiredDescription
emailstringrequiredValid email address
passwordstringrequiredAccount 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.refreshToken

POST /auth/refresh

Exchange a refresh token for new access and refresh tokens. Access tokens expire after 15 minutes; refresh tokens last 30 days.

FieldTypeRequiredDescription
refreshTokenstringrequiredA 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.refreshToken

GET /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):

json
{
  "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.apiKeyPrefix

Response (200):

json
{
  "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):

json
{
  "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

LimitFreeStarterProductionGrowth
Max jobs31030100
Daily runs2002,00012,00050,000
Monthly runs2,00020,000120,000500,000
Max retries35710
Request timeout30s60s120s300s
Alert configs131050
Log retention1 day3 days14 days30 days
Concurrency12515

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):

json
{
  "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 }
    }
  }
}