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| Field | Type | Default | Description |
|---|---|---|---|
name | string | — | Optional label |
scopes | string[] | ['read', 'write'] | read, write, or both |
expiresInDays | number | 30 | Token lifetime (1–365 days) |
allowedIps | string[] | — | 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."
}Create Magic Link
POST /api/organizations/:orgId/portal/webhook-applications/:appId/magic-link| Field | Type | Default | Description |
|---|---|---|---|
expiresInMinutes | number | 60 | Token lifetime (1–1440 minutes) |
scopes | string[] | ['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/tokensReturns all non-session tokens (magic link tokens are excluded).
Revoke Token
DELETE /api/organizations/:orgId/portal/tokens/:tokenIdSoft-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/applicationResponse:
{
"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/endpointsRequires 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/endpointsRequires scope: write
| Field | Type | Required | Description |
|---|---|---|---|
url | string | Yes | Webhook URL (HTTPS required, HTTP allowed for localhost) |
description | string | No | Max 500 characters |
successStatusCodes | (number | string)[] | No | Custom success codes (e.g., [200, "2xx"]) |
backoffType | string | No | exponential, linear, or fixed |
retryDelays | number[] | No | Custom 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/:idRequires scope: write
| Field | Type | Description |
|---|---|---|
url | string | New webhook URL |
description | string | null | Updated description |
isDisabled | boolean | Enable/disable the endpoint |
successStatusCodes | (number | string)[] | null | Custom success codes |
backoffType | string | null | Retry backoff strategy |
retryDelays | number[] | null | Custom retry delays |
Delete Endpoint
DELETE /portal/endpoints/:idRequires scope: write
Rotate Signing Secret
POST /portal/endpoints/:id/rotate-secretRequires 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/testRequires scope: write
| Field | Type | Required | Description |
|---|---|---|---|
eventType | string | No | Event 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/verifyRequires 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/replayRequires 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-failedRequires 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-typesRequires 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/subscriptionsRequires scope: read
Query parameter: endpointId (optional) — filter by endpoint.
Create Subscription
POST /portal/subscriptionsRequires scope: write
| Field | Type | Required | Description |
|---|---|---|---|
endpointId | string | Yes | Target endpoint |
eventTypeId | string | Yes | Event type to subscribe to |
Returns 409 if the subscription already exists.
Delete Subscription
DELETE /portal/subscriptions/:idRequires scope: write
Messages
List Messages
GET /portal/messagesRequires scope: read
| Query Param | Type | Default | Description |
|---|---|---|---|
status | string | — | Filter: pending, success, failed, exhausted |
limit | number | 50 | Max 100 |
Get Message Attempts
GET /portal/messages/:id/attemptsRequires 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"
}| Status | Meaning |
|---|---|
400 | Invalid input (validation error) |
401 | Invalid or expired token |
403 | Insufficient scope |
404 | Resource not found or doesn't belong to this application |
409 | Conflict (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.