Skip to main content

Endpoint calls

An endpoint is a released workflow called over HTTP. Its Response node supplies an HTTP body, status and content type. Calls are synchronous by default:

POST /api/endpoints/<processPublicId>
Authorization: Bearer <user-access-token>
Content-Type: application/json

{"customerId":42}

Deployed Honeybase apps use their app session on the same origin, and may call only the endpoints the app declares (the app editor's Endpoints list, the apps.setAppEndpoints mutation, or the set_app_endpoints tool); a call to any other endpoint is refused with 403 {"code":"app_endpoint_not_declared"}, whose message names the endpoint to declare. Scoped API keys require endpoint:invoke for the endpoint; a GraphQL grant alone does not grant endpoint access. Unknown, paused, unreleased and other-organization endpoints return 404. Request contracts are validated before execution (400 on mismatch). The request body is limited to 256 KiB; Response bodies to 1 MiB.

Public endpoints (no login)​

An endpoint can also be made public, so a static website on another origin can call it without any login — a lookup form, an RSVP page, a waitlist. It is off by default. Turning it on mints an unguessable token, and the endpoint becomes callable at:

POST /api/public/endpoints/<token>
Content-Type: application/json

{"guestId":"g-7f3a"}

The call is synchronous and answers with the Response node's body and status, exactly like the authenticated call (Prefer: respond-async is ignored). Unknown, revoked, paused and unreleased endpoints return 404. In the workflow, Trigger.result.caller.kind is "public", and caller.userId and caller.email are null.

Enable, rotate or revoke the token with the setEndpointPublicAccess mutation, or with the MCP update_process_settings tool (publicAccess: "enable" | "rotate" | "disable", publicAllowedOrigins, publicAnyOrigin, which need the release scope), or from the endpoint's page in the app (the Public access card). All of them require the Manage releases and Build workflows permissions; endpointPublicAccess reads the current token.

mutation {
processes {
setEndpointPublicAccess(
processPublicId: "019..."
enabled: true
allowedOrigins: ["https://example.com"]
) { token path allowedOrigins allowAnyOrigin }
}
}

Browser origins. Pages may call a public endpoint only from the origins listed in allowedOrigins (exact scheme://host[:port]; at most 20; the list is replaced on each update). Those origins get CORS headers on the answer and on the OPTIONS preflight; a request carrying any other Origin header is refused with 403 {"code":"origin_not_allowed"} before the workflow runs. Requests with no Origin header (servers, curl) are allowed — the origin list stops other websites from using your endpoint, it is not authentication. Credentials (cookies) are never accepted on this route.

An empty list ([]) means no website may call the endpoint from a browser, and it is what enabling stores when you send no list. To let any website call it, send allowAnyOrigin: true instead of a list (MCP: publicAnyOrigin: true); it cannot be combined with a non-empty list, and allowAnyOrigin: false on its own means "no website". An internationalised domain may be written as typed (https://bücher.example): it is stored in the punycode form browsers send.

rotate: true replaces the URL of an endpoint that is already public. On one that is not, it is refused unless you also send enabled: true. Choose "any" only for an endpoint whose effects are harmless when triggered by a page you do not control: every site on the web can then make its visitors' browsers call it.

Limits. Public calls are rate-limited per IP address (60/minute by default), then per token (120/minute), on top of the organization's endpoint limits. A 429 carries Retry-After. At most 5 public calls to one endpoint run at the same time; the rest wait briefly, then get 429 with Retry-After. A busy public page therefore cannot use up the concurrency your organization's own callers need.

Treat the token like a password

Anyone who has the URL can call the endpoint, as often as the limits allow, and a URL embedded in a public web page is visible to everyone who opens it. Return only what a stranger may see (look up records by an opaque id rather than a name or email, and return the minimum fields), validate input with a request contract, and assume any write the workflow makes can be triggered by anyone. Rotate the token if it leaks; the old URL stops working immediately, and calls already running on it stop before their next step.

Capacity and retries​

The default organization limit is 20 active endpoint calls across all server replicas, shared by synchronous calls and executing async runs. Honeybase can adjust it per organization. This is concurrency, distinct from rate limits: 600 calls/minute per organization and 300/minute per endpoint by default.

An org at capacity waits briefly, at most two seconds, in a bounded queue (up to 20 waiting calls/org). If capacity remains unavailable or the queue is full, the call returns 429, with Retry-After: 1 as a retry hint. Wait at least that long and add jitter; capacity is not guaranteed at that time. A pod at its own limit, or unable to obtain shared admission, returns 503 with Retry-After. Rejected calls do not execute a workflow.

For rate-limit 429s, Retry-After is the remaining window lifetime. Where available, X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset identify the governing rate bucket. Reset is a Unix timestamp in seconds. Avoid blindly repeating a POST after a lost connection: earlier nodes may have already produced side effects.

Per-endpoint execution timeouts default to 25 seconds and can be set up to 270 seconds. Runs waiting for JS memory capacity remain subject to their execution timeout. Synchronous calls have no Run History entry.

Async calls​

Choose async explicitly for background work:

POST /api/endpoints/<processPublicId>
Prefer: respond-async
Authorization: Bearer <user-access-token>
Content-Type: application/json

{"customerId":42}

?mode=async is an alternative. Acceptance returns:

HTTP/1.1 202 Accepted
Location: /api/endpoints/runs/019...-run-uuid
Preference-Applied: respond-async
Cache-Control: no-store

{"runId":"019...-run-uuid","status":"queued"}

The run is recorded immediately in Run History and uses its normal cancellation controls. It executes on the worker queue; acceptance does not mean it has started or finished. An org can have at most 100 outstanding async endpoint runs. A full backlog returns 429. A run waiting more than 30 minutes fails without executing. Async mode has the endpoint's same execution timeout; it does not extend the maximum of 270 seconds.

Poll the Location with the same starting user or same starting API key. API keys additionally require endpoint:results:read. Owning the key, being an administrator or having Run History access does not make a caller the starter. A different caller or unknown run receives 404. Results are not cached.

{
"runId": "019...-run-uuid",
"status": "complete",
"expiresAt": "2026-10-21T20:00:00Z",
"response": {
"status": 201,
"body": "{\"accepted\":true}",
"contentType": "application/json"
}
}

Polling returns HTTP 200 for a known retained run, including a failed run. The Response node's status is inside response, not the polling HTTP status. The body is a string preserving the synchronous response. While queued or running, response is null. A run ending without a Response node produces status 204; workflow errors produce 502 and execution timeouts 504. cancelled and skipped are terminal statuses with no response. Multiple reachable Response nodes use the same deterministic selection as sync mode: the first node name alphabetically.

Results expire seven days after acceptance, matching node-result retention. An authenticated starter receives 410 after expiry while ownership metadata is retained; after cleanup the run result returns 404. Run History lifecycle metadata may remain after response data expires. Use bounded polling with backoff and stop when a terminal status appears.