Portal API Reference

The portal API is authenticated with portal tokens (whpt_xxx). All endpoints are scoped to the application associated with the token.

Base URL: https://api.hookbase.app

Authentication: Authorization: Bearer whpt_xxx

Token Management

These endpoints use standard API authentication (not portal tokens). Use them server-side to create and manage portal tokens.

Create Portal Token

POST /api/organizations/:orgId/portal/webhook-applications/:appId/tokens
FieldTypeDefaultDescription
namestringOptional label
scopesstring[]['read', 'write']read, write, or both
expiresInDaysnumber30Token lifetime (1–365 days)
allowedIpsstring[]Optional IP allowlist

Response (201):

{
  "data": {
    "id": "tok_abc123",
    "token": "whpt_abc123...",
    "name": "Production",
    "scopes": ["read", "write"],
    "expiresAt": "2026-04-13T00:00:00.000Z",
    "createdAt": "2026-03-14T00:00:00.000Z"
  },
  "warning": "Save this token now. It will not be shown again."
}
POST /api/organizations/:orgId/portal/webhook-applications/:appId/magic-link
FieldTypeDefaultDescription
expiresInMinutesnumber60Token lifetime (1–1440 minutes)
scopesstring[]['read', 'write']Permissions

Response (201):

{
  "data": {
    "url": "https://www.hookbase.app/portal/whpt_abc123...",
    "token": "whpt_abc123...",
    "expiresAt": "2026-03-14T15:30:00.000Z",
    "expiresInMinutes": 60,
    "scopes": ["read", "write"]
  }
}

List Tokens

GET /api/organizations/:orgId/portal/webhook-applications/:appId/tokens

Returns all non-session tokens (magic link tokens are excluded).

Revoke Token

DELETE /api/organizations/:orgId/portal/tokens/:tokenId

Soft-deletes the token. Revoked tokens are immediately invalid.


Portal Endpoints

All endpoints below use portal token authentication (Bearer whpt_xxx).

Application

Get Application Info

GET /portal/application

Response:

{
  "data": {
    "id": "app_abc123",
    "name": "My SaaS App",
    "externalId": "customer-123",
    "totalEndpoints": 3,
    "totalMessagesSent": 1250,
    "totalMessagesFailed": 12,
    "scopes": ["read", "write"]
  }
}

Endpoints

List Endpoints

GET /portal/endpoints

Requires scope: read

Response:

{
  "data": [
    {
      "id": "ep_abc123",
      "url": "https://example.com/webhooks",
      "description": "Production endpoint",
      "secretPrefix": "whsec_abc123...",
      "isDisabled": false,
      "circuitState": "closed",
      "totalMessages": 150,
      "totalSuccesses": 148,
      "totalFailures": 2,
      "subscriptionCount": 5,
      "isVerified": true,
      "verifiedAt": "2026-03-10T12:00:00.000Z",
      "createdAt": "2026-03-01T00:00:00.000Z"
    }
  ]
}

Create Endpoint

POST /portal/endpoints

Requires scope: write

FieldTypeRequiredDescription
urlstringYesWebhook URL (HTTPS required, HTTP allowed for localhost)
descriptionstringNoMax 500 characters
successStatusCodes(number | string)[]NoCustom success codes (e.g., [200, "2xx"])
backoffTypestringNoexponential, linear, or fixed
retryDelaysnumber[]NoCustom retry delays in seconds

Response (201):

{
  "data": {
    "id": "ep_abc123",
    "url": "https://example.com/webhooks",
    "description": "Production",
    "secret": "whsec_abc123...",
    "createdAt": "2026-03-14T00:00:00.000Z"
  },
  "warning": "Save the signing secret now. It will not be shown again."
}

Update Endpoint

PATCH /portal/endpoints/:id

Requires scope: write

FieldTypeDescription
urlstringNew webhook URL
descriptionstring | nullUpdated description
isDisabledbooleanEnable/disable the endpoint
successStatusCodes(number | string)[] | nullCustom success codes
backoffTypestring | nullRetry backoff strategy
retryDelaysnumber[] | nullCustom retry delays

Delete Endpoint

DELETE /portal/endpoints/:id

Requires scope: write

Rotate Signing Secret

POST /portal/endpoints/:id/rotate-secret

Requires scope: write

Generates a new signing secret. The old secret remains valid for 24 hours (grace period).

Response:

{
  "data": {
    "secret": "whsec_newSecret...",
    "secretVersion": 2,
    "previousSecretExpiresAt": "2026-03-15T00:00:00.000Z"
  },
  "warning": "Save the new signing secret. The old secret will remain valid for 24 hours."
}

Test Events

Send Test Event

POST /portal/endpoints/:id/test

Requires scope: write

FieldTypeRequiredDescription
eventTypestringNoEvent type to test (uses default if omitted)

Sends a test webhook to the endpoint and returns the delivery result.

Response:

{
  "success": true,
  "testEvent": {
    "id": "evt_test_abc123",
    "type": "test.event",
    "timestamp": "2026-03-14T00:00:00.000Z"
  },
  "delivery": {
    "status": "success",
    "responseStatus": 200,
    "responseTimeMs": 145,
    "responseBody": "{\"ok\":true}",
    "errorMessage": null
  },
  "signature": {
    "header": "x-hookbase-signature",
    "value": "v1=abc123...",
    "timestamp": 1710374400
  }
}

Verification

Verify Endpoint

POST /portal/endpoints/:id/verify

Requires scope: write

Sends a challenge request to the endpoint. The endpoint must respond with 200 OK and a JSON body containing the challenge value:

Challenge request sent to the endpoint:

{
  "type": "endpoint.verification",
  "challenge": "random-challenge-token"
}

Expected response from the endpoint:

{
  "challenge": "random-challenge-token"
}

API Response:

{
  "data": {
    "verified": true,
    "error": null
  }
}

Replay

Replay Single Message

POST /portal/messages/:id/replay

Requires scope: write

Only failed, exhausted, or DLQ messages can be replayed. Creates a new message and queues it for delivery.

Response (201):

{
  "data": {
    "originalMessageId": "msg_abc123",
    "newMessageId": "msg_def456",
    "status": "queued"
  }
}

Bulk Replay Failed Messages

POST /portal/endpoints/:id/replay-failed

Requires scope: write

Replays all failed/exhausted/DLQ messages for an endpoint (capped at 100).

Response (201):

{
  "data": {
    "replayed": 5,
    "newMessageIds": ["msg_1", "msg_2", "msg_3", "msg_4", "msg_5"]
  }
}

Event Types

List Event Types

GET /portal/event-types

Requires scope: read

Returns event types configured for the organization, including schemas and example payloads.

Response:

{
  "data": [
    {
      "id": "et_abc123",
      "name": "order.created",
      "displayName": "Order Created",
      "description": "Fired when a new order is placed",
      "category": "Orders",
      "schema": "{\"type\":\"object\",...}",
      "examplePayload": "{\"orderId\":\"123\",...}",
      "documentationUrl": "https://docs.example.com/events/order-created"
    }
  ]
}

Subscriptions

List Subscriptions

GET /portal/subscriptions

Requires scope: read

Query parameter: endpointId (optional) — filter by endpoint.

Create Subscription

POST /portal/subscriptions

Requires scope: write

FieldTypeRequiredDescription
endpointIdstringYesTarget endpoint
eventTypeIdstringYesEvent type to subscribe to

Returns 409 if the subscription already exists.

Delete Subscription

DELETE /portal/subscriptions/:id

Requires scope: write


Messages

List Messages

GET /portal/messages

Requires scope: read

Query ParamTypeDefaultDescription
statusstringFilter: pending, success, failed, exhausted
limitnumber50Max 100

Get Message Attempts

GET /portal/messages/:id/attempts

Requires scope: read

Returns delivery attempts for a message, including response status, timing, and error details.

Response:

{
  "data": [
    {
      "id": "att_abc123",
      "messageId": "msg_abc123",
      "attemptNumber": 1,
      "status": "failed",
      "responseStatus": 500,
      "responseTimeMs": 2340,
      "errorType": "http_error",
      "errorMessage": "Internal Server Error",
      "createdAt": "2026-03-14T00:00:00.000Z",
      "completedAt": "2026-03-14T00:00:02.340Z"
    }
  ]
}

Error Responses

All error responses follow this format:

{
  "error": "Error description"
}
StatusMeaning
400Invalid input (validation error)
401Invalid or expired token
403Insufficient scope
404Resource not found or doesn't belong to this application
409Conflict (e.g., duplicate subscription)

SDK Client

The @hookbase/portal package exports a PortalApiClient class that wraps all these endpoints:

import { PortalApiClient } from '@hookbase/portal';
 
const client = new PortalApiClient('https://api.hookbase.app', 'whpt_abc123...');
 
const endpoints = await client.getEndpoints();
const testResult = await client.testEndpoint('ep_abc123');
const verifyResult = await client.verifyEndpoint('ep_abc123');

See the Hooks page for React-friendly wrappers around this client.