HookSniff API Reference

June 23, 2026 · View on GitHub

Base URL: https://hooksniff-api-499907444852.europe-west1.run.app/v1 Local: http://localhost:3000/v1

Authentication: All endpoints (except /auth/* and /health) require:

Authorization: Bearer hr_live_YOUR_API_KEY

Table of Contents


Authentication

Register

POST /v1/auth/register

Create a new account. Returns a JWT token and API key (shown only once).

Request:

{
  "email": "user@example.com",
  "password": "SecurePass123!"
}

Response 200:

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "customer": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "user@example.com",
    "api_key": "hr_live_a1b2c3d4e5f6g7h8",
    "plan": "free",
    "webhook_limit": 1000,
    "webhook_count": 0,
    "created_at": "2026-05-06T00:00:00Z"
  }
}

Errors:

CodeCondition
400Invalid email, password < 8 chars, or email already registered
429Rate limit exceeded

Login

POST /v1/auth/login

Authenticate and get a JWT token. Does not return the API key.

Request:

{
  "email": "user@example.com",
  "password": "SecurePass123!"
}

Response 200:

{
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "customer": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "user@example.com",
    "plan": "free",
    "webhook_limit": 1000,
    "webhook_count": 0,
    "created_at": "2026-05-06T00:00:00Z"
  }
}

Errors:

CodeCondition
400Invalid input
401Wrong credentials

Endpoints

List Endpoints

GET /v1/endpoints

Returns all webhook endpoints for the authenticated customer.

Response 200:

[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "url": "https://myapp.com/webhook",
    "description": "Order notifications",
    "is_active": true,
    "signing_secret": "whsec_abc123...",
    "retry_policy": {
      "max_attempts": 3,
      "backoff": "exponential",
      "initial_delay_secs": 10,
      "max_delay_secs": 3600
    },
    "created_at": "2026-05-06T00:00:00Z"
  }
]

Create Endpoint

POST /v1/endpoints

Register a new webhook endpoint. A signing secret is auto-generated.

Request:

{
  "url": "https://myapp.com/webhook",
  "description": "Order notifications",
  "retry_policy": {
    "max_attempts": 5,
    "backoff": "exponential",
    "initial_delay_secs": 30,
    "max_delay_secs": 3600
  },
  "custom_headers": {
    "X-Custom-Header": "value"
  },
  "event_filter": "order.*"
}

Response 200:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "url": "https://myapp.com/webhook",
  "description": "Order notifications",
  "is_active": true,
  "signing_secret": "whsec_abc123def456...",
  "retry_policy": {
    "max_attempts": 5,
    "backoff": "exponential",
    "initial_delay_secs": 30,
    "max_delay_secs": 3600
  },
  "created_at": "2026-05-06T00:00:00Z"
}

Errors:

CodeCondition
400URL must start with http:// or https://
400Custom headers must start with X-
403Internal/private URLs are blocked (SSRF protection)

Get Endpoint

GET /v1/endpoints/{id}

Response 200: Same as endpoint object above.

Errors: 401 Unauthorized, 404 Not found.


Update Endpoint

PUT /v1/endpoints/{id}

Partial update — only provided fields are modified.

Request:

{
  "url": "https://myapp.com/webhooks/v2",
  "description": "Updated endpoint",
  "is_active": false,
  "retry_policy": {
    "max_attempts": 10,
    "backoff": "exponential"
  }
}

Response 200: Updated endpoint object.

Errors: 400 Bad request, 401 Unauthorized, 404 Not found.


Delete Endpoint

DELETE /v1/endpoints/{id}

Permanently removes the endpoint and all delivery history.

Response 200:

{ "deleted": true }

Errors: 401 Unauthorized, 404 Not found.


Rotate Signing Secret

POST /v1/endpoints/{id}/rotate-secret

Generates a new signing secret. Old secret remains valid for 24 hours.

Response 200:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "signing_secret": "whsec_newsecret123...",
  "old_secret_valid_until": "2026-05-07T12:00:00Z",
  "message": "Secret rotated. Old secret remains valid for 24 hours."
}

Webhooks

Send Webhook

POST /v1/webhooks

Queues a webhook for async delivery. Returns immediately.

Request:

{
  "endpoint_id": "550e8400-e29b-41d4-a716-446655440000",
  "event": "order.created",
  "data": {
    "order_id": "12345",
    "amount": 99.99,
    "currency": "USD"
  }
}

Headers:

Idempotency-Key: unique-request-id-123  (optional, for exactly-once delivery)

Response 200:

{
  "id": "wh_xyz789",
  "endpoint_id": "550e8400-e29b-41d4-a716-446655440000",
  "event": "order.created",
  "status": "pending",
  "attempt_count": 0,
  "response_status": null,
  "replay_count": 0,
  "created_at": "2026-05-06T02:50:00Z"
}

Errors:

CodeCondition
400Invalid payload or event type
404Endpoint not found or inactive
413Payload exceeds max size (default 1 MB)
429Monthly limit reached

Send Batch Webhooks

POST /v1/webhooks/batch

Send up to 100 webhooks in one request. Partial failures are reported.

Request:

{
  "webhooks": [
    {
      "endpoint_id": "...",
      "event": "order.created",
      "data": { "order_id": "001" }
    },
    {
      "endpoint_id": "...",
      "event": "order.shipped",
      "data": { "order_id": "002" }
    }
  ]
}

Response 200:

{
  "deliveries": [
    { "id": "wh_001", "status": "pending", "..." : "..." },
    { "id": "wh_002", "status": "pending", "..." : "..." }
  ],
  "errors": [
    { "index": 5, "error": "Endpoint not found or inactive" }
  ]
}

List Webhooks

GET /v1/webhooks?page=1&per_page=20&status=delivered

Query Parameters:

ParamDefaultDescription
page1Page number
per_page20Items per page (max 100)
statusFilter: pending, delivered, failed, filtered

Response 200:

{
  "deliveries": [...],
  "total": 150,
  "page": 1,
  "per_page": 20
}

Get Webhook

GET /v1/webhooks/{id}

Response 200: Delivery object with status, attempt count, response status.


Replay Webhook

POST /v1/webhooks/{id}/replay

Creates a new delivery from the original payload. Endpoint must be active.

Response 200: New delivery object with status: "pending".

Errors: 400 Endpoint inactive, 404 Not found.


Get Delivery Attempts

GET /v1/webhooks/{id}/attempts

Returns all delivery attempts (including retries) for a webhook.

Response 200:

[
  {
    "id": "att_001",
    "delivery_id": "wh_xyz789",
    "attempt_number": 1,
    "status_code": 500,
    "response_body": "Internal Server Error",
    "duration_ms": 1234,
    "error_message": null,
    "created_at": "2026-05-06T02:50:00Z"
  },
  {
    "id": "att_002",
    "delivery_id": "wh_xyz789",
    "attempt_number": 2,
    "status_code": 200,
    "response_body": "{\"ok\": true}",
    "duration_ms": 456,
    "error_message": null,
    "created_at": "2026-05-06T02:51:00Z"
  }
]

Export Webhook Logs

GET /v1/webhooks/export?format=csv&status=failed&date_from=2026-05-01&date_to=2026-05-06

Query Parameters:

ParamDefaultDescription
formatjsonjson or csv
statusFilter by status
date_fromStart date (ISO 8601 or YYYY-MM-DD)
date_toEnd date

Max 10,000 records per export.


API Keys

List API Keys

GET /v1/api-keys

Returns all keys with only their prefix visible (first 15 chars).

Response 200:

[
  {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "prefix": "hr_live_a1b2c3d...",
    "name": "Production",
    "created_at": "2026-05-06T00:00:00Z",
    "last_used_at": "2026-05-06T12:00:00Z",
    "is_active": true
  }
]

Create API Key

POST /v1/api-keys

Request:

{
  "name": "Production API Key"
}

Response 200:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "key": "hr_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "prefix": "hr_live_a1b2c3d",
  "message": "Save this key — it won't be shown again."
}

⚠️ The full key is only returned once. Save it immediately.


Delete API Key

DELETE /v1/api-keys/{id}

Immediately revokes the key.

Response 200:

{ "deleted": true }

Rotate API Key

POST /v1/api-keys/{id}/rotate

Generates a new key value. Old key is immediately invalidated — no grace period.

Response 200:

{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "key": "hr_live_newkey123456789abcdef",
  "prefix": "hr_live_newkey1",
  "message": "Key rotated. Save the new key — it won't be shown again."
}

Billing

Get Subscription

GET /v1/billing/subscription

Response 200:

{
  "plan": "pro",
  "status": "active",
  "stripe_subscription_id": "sub_abc123",
  "webhook_limit": 100000,
  "endpoint_limit": 2147483647,
  "retention_days": 180,
  "monthly_price_cents": 4900
}

Upgrade Plan

POST /v1/billing/upgrade

Creates a Stripe Checkout session. Redirect customer to the returned URL.

Request:

{ "plan": "pro" }

Response 200:

{
  "checkout_url": "https://checkout.stripe.com/pay/cs_test_...",
  "message": "Redirecting to Stripe Checkout for pro plan (\$49.00/mo)"
}

Errors:

CodeCondition
400Cannot downgrade (use portal) or enterprise (contact sales)

Open Customer Portal

POST /v1/billing/portal

Returns a Stripe Customer Portal URL for managing subscriptions.

Response 200:

{ "url": "https://billing.stripe.com/session/..." }

Errors: 400 No Stripe customer (upgrade first).


Get Usage

GET /v1/billing/usage

Response 200:

{
  "plan": "pro",
  "webhooks": { "used": 1250, "limit": 100000, "remaining": 98750 },
  "endpoints": { "used": 3, "limit": 2147483647, "remaining": 2147483644 },
  "rate_limit": { "requests_per_minute": 1000 },
  "period": { "start": "2026-05-01", "end": "2026-05-06" }
}

Stats

Get Statistics

GET /v1/stats

Response 200:

{
  "total_deliveries": 1500,
  "delivered": 1450,
  "failed": 30,
  "pending": 20,
  "success_rate": 96.67,
  "endpoints_count": 5
}

Endpoint Health

List Endpoint Health

GET /v1/endpoint-health

Response 200:

[
  {
    "id": "...",
    "url": "https://myapp.com/webhook",
    "description": "Order notifications",
    "is_active": true,
    "health_status": "healthy",
    "success_rate": 99.2,
    "avg_response_ms": 234,
    "p95_response_ms": 468,
    "p99_response_ms": 702,
    "total_deliveries": 500,
    "successful": 496,
    "failed": 4,
    "consecutive_failures": 0,
    "last_success_at": "2026-05-06T12:00:00Z",
    "last_failure_at": null,
    "uptime_24h": 99.2,
    "uptime_7d": 98.8
  }
]

Health Status:

StatusCondition
healthy< 3 consecutive failures, success rate ≥ 95%
degraded3-4 consecutive failures OR success rate < 95%
unhealthy≥ 5 consecutive failures

Get Endpoint Health

GET /v1/endpoint-health/{id}

Returns health details for a single endpoint.


Cortex — Intelligent Protection System

Cortex is HookSniff's ML-powered system that monitors webhook endpoints, detects anomalies, predicts failures, and auto-heals issues.

EndpointMethodDescription
/v1/cortex/healthGETCortex system health & metrics
/v1/cortex/anomaliesGETRecent anomaly scores
/v1/cortex/anomalies/highGETHigh-severity anomalies
/v1/cortex/healing/actionsGETAuto-healing action history
/v1/cortex/predictionsGETFailure predictions
/v1/cortex/insightsGETActive insights
/v1/cortex/profilesGETEndpoint behavioral profiles
/v1/cortex/ml/qualityGETML model quality scores
/v1/cortex/ml/quality/resetPOSTReset degraded models
/v1/cortex/drift/eventsGETConcept drift detection events
/v1/cortex/models/platform-summaryGETPlatform model health overview
/v1/cortex/ab-testsGETA/B test configurations
/v1/cortex/chaos/scenariosGETChaos engineering scenarios
/v1/cortex/automl/trials/{id}GETAutoML optimization trials
/v1/cortex/tracing/performanceGETPipeline performance metrics

Webhook Delivery Format

When HookSniff delivers a webhook to your endpoint:

POST https://myapp.com/webhook HTTP/1.1
Content-Type: application/json
webhook-signature: v1,<base64(hmac)>
webhook-id: wh_xyz789
webhook-attempt: 1

{
  "event": "order.created",
  "data": {
    "order_id": "12345",
    "amount": 99.99
  },
  "timestamp": "2026-05-06T02:50:00Z"
}

Signature Verification

Verify the webhook-signature header using Standard Webhooks HMAC-SHA256:

const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}
import hmac, hashlib

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(
        secret.encode(), payload, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)
func verifySignature(payload []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(signature), []byte(expected))
}

Your Endpoint Should

  1. Return 2xx for success (HookSniff marks as delivered)
  2. Return 4xx/5xx or timeout → retry with exponential backoff
  3. Verify the signature before processing
  4. Respond within 30 seconds (default timeout)

Error Handling

All errors return a consistent format:

{
  "error": {
    "code": "BAD_REQUEST",
    "message": "URL must start with http:// or https://"
  }
}

Error Codes

HTTPCodeDescription
400BAD_REQUESTInvalid input, validation failed
401UNAUTHORIZEDMissing or invalid API key
403FORBIDDENInternal URL blocked (SSRF protection)
404NOT_FOUNDResource not found
413PAYLOAD_TOO_LARGEPayload exceeds 1 MB
429RATE_LIMIT_EXCEEDEDMonthly limit reached
500INTERNAL_ERRORServer error (safe to retry)

Rate Limits

PlanRequests/minWebhooks/monthEndpointsRetention
Free1001,00057 days
Pro ($49/mo)1,00050,0005030 days
Business ($149/mo)10,000500,00050090 days

Rate limit headers (included in every response):

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 995
X-RateLimit-Reset: 45

Idempotency

The POST /v1/webhooks endpoint supports idempotency:

POST /v1/webhooks
Idempotency-Key: my-unique-key-123
  • Same key within 24 hours → returns cached response
  • Prevents duplicate deliveries on network retries
  • Keys are scoped per customer

Health Check

GET /v1/health

No authentication required.

{
  "status": "ok",
  "service": "hooksniff-api",
  "version": "0.1.0"
}