Skip to content
One platform. From customer conversation to completed work.Take a closer look
Documentation menu

API reference

Connect your product to the conversation.

Read recent tickets and send customer requests into NodeHub from your own backend. Use a scoped API key with the workspace and brand your integration should access.

Product preview: workspace access opens at launch. Follow the launch.

Start with a scoped key

  1. Create a service identity.A workspace owner or administrator opens Settings > Machine users & API keys, then selects New machine user.
  2. Choose the brand it should access.A selected brand limits reads to that brand and is required for ticket creation. A machine user with No brand can read across its workspace, but cannot create tickets.
  3. Issue a key with the scopes you need.Choose Keys, then Issue new key. Use thread:read for reading, thread:create for creating, or both when necessary.
  4. Save the secret on your server.The full key is shown once. NodeHub stores a hash of the secret. Keep the issued value intact and load it from your backend's secret store.

Send Authorization: Bearer YOUR_API_KEY on each request. The origin is the HTTPS address of your NodeHub installation, with no trailing slash. API requests do not require a browser session or a CSRF token when authenticated with a valid API key.

The key determines the workspace; its machine user determines the brand. Do not send workspace or brand identifiers in a header or payload to select another account.

Keys belong on your backend, never in a public browser bundle. Disabling a machine user stops all its keys. A key labeled test does not create an isolated sandbox; use a dedicated workspace or brand and controlled records when testing.

GET/api/v1/threads

Read recent tickets

Requires thread:read. Returns up to 50 non-deleted, non-test tickets, ordered by updatedAt descending. The response contains a data array. An empty result is { "data": [] }.

Read-only request, cURL
# Set NODEHUB_ORIGIN to your NodeHub installation's HTTPS origin.
# Load NODEHUB_API_KEY from your server's secret store.
curl --fail-with-body --silent --show-error \
  "$NODEHUB_ORIGIN/api/v1/threads" \
  --header "Authorization: Bearer $NODEHUB_API_KEY" \
  --header "Accept: application/json"
Illustrative 200 response, example IDs and data
{
  "data": [
    {
      "id": "thread_example",
      "ref": "SUP-123",
      "title": "Help with account access",
      "status": "TODO",
      "priority": "NORMAL",
      "updatedAt": "2026-10-07T12:00:00.000Z"
    }
  ]
}

updatedAt is an ISO 8601 timestamp. Status values are TODO, IN_PROGRESS, WAITING_ON_CUSTOMER, SNOOZED, RESOLVED, and CLOSED. Priority values are LOW, NORMAL, HIGH, and URGENT.

This endpoint currently provides the 50 most recently updated tickets. It has no pagination cursor, total count, filters, or custom limit. Do not use it as a full-history export or a lossless event feed.

POST/api/v1/threads

Create a customer request

Requires thread:create and a brand selected on the machine user. Send Content-Type: application/json, your bearer key, and an Idempotency-Key header of 1 to 200 characters. Use a unique value from your source event, or a UUID, and retain it for retries.

This operation creates a real ticket and its first customer message. It finds or creates the contact by email within the selected brand and, when supplied, the customer company. If an email matches more than one contact, send companyExternalId to identify the customer company. The ticket's committed events can trigger configured workflows and webhooks.

The API key must belong to an active machine user with the required scope in the intended workspace and brand.

Illustrative JSON request body
{
  "subject": "Help with account access",
  "body": "I cannot access my account after resetting my password.",
  "customer": {
    "email": "[email protected]",
    "fullName": "Alex Example"
  },
  "priority": "normal",
  "type": "question",
  "labels": ["account-access"]
}
Accepted ticket fields
FieldRequiredFormat and behavior
subjectRequiredString, 3 to 300 characters after trimming.
bodyRequiredString, 1 to 50,000 characters after trimming. Becomes the first customer message.
customer.emailRequiredValid email address, at most 300 characters. Matched without case inside the selected customer company, when supplied. An ambiguous email requires companyExternalId.
customer.fullNameOptionalString, 1 to 200 characters. Updates the matching contact's name when provided.
customer.externalIdOptionalString, at most 200 characters. Sets an external ID on a new contact or one without an external ID; it does not replace an existing value.
tenantExternalIdOptionalString, at most 240 characters. Must identify an existing tenant in the key's brand. The contact joins that tenant and the ticket belongs to it. Send this or companyExternalId, not both.
companyExternalIdOptionalString, at most 240 characters. Must identify an existing customer company in the key's brand. Contacts are matched inside that company, and when it has exactly one tenant, the ticket belongs to it.
priorityOptionallow, normal, high, or urgent. When omitted, the customer's tier default applies, otherwise normal.
typeOptionalquestion, bug, feature_request, billing, or other. Defaults to question.
labelsOptionalArray of up to 10 names, each 1 to 60 characters. Repeated names are deduplicated; new names create workspace labels. Deleted labels are skipped.

These fields cover standard ticket intake. The endpoint also accepts a structured alert for operational integrations, which requires tenantExternalId or companyExternalId. Send priority and type values in lowercase. Responses use uppercase status and priority values. A contact's external ID must not already belong to another contact in the workspace.

Illustrative 201 response, example IDs and data
{
  "id": "thread_example",
  "ref": "SUP-123",
  "status": "TODO",
  "priority": "NORMAL",
  "customerId": "customer_example",
  "companyId": null,
  "tenantId": null
}

A new ticket returns 201. A successful replay returns 200 with the existing ticket's current result fields. companyId and tenantId are null when the contact has no company or tenant. The create response is a single object, without the read endpoint's data wrapper.

Make retries predictable

The idempotency key is scoped to the workspace and brand, including across keys for that brand. Reusing it with the same validated payload returns the existing ticket without creating another. Reusing it with a different payload returns 409. Keep the same body and key together for retries; generate a new key only for a new request.

Ticket creation allows 60 authenticated attempts per minute per API key. Retries and requests that later fail validation count toward the limit. A 429 response includes Retry-After in seconds. The read handler does not advertise a pagination or rate-limit header contract.

If a request times out, the ticket may already have been committed. Retry with the same key and payload after a bounded delay, then inspect the returned ticket ID. Do not respond to a timeout by issuing a new idempotency key.

Error responses

Route errors return JSON with an error message and usually a code. Validation responses can also include issues. Authentication middleware and server failures may omit code, so handle the HTTP status first.

Illustrative missing-scope response, HTTP 403
{
  "error": "missing scope: thread:create",
  "code": "forbidden"
}
Response status reference
StatusCodeWhat it means
400invalid_bodyMalformed JSON, a missing required field, an unsupported value, or an unknown top-level property. Validation errors may include an issues array.
400idempotency_key_requiredThe Idempotency-Key header is missing, empty, or longer than 200 characters.
400invalid_requestA referenced tenant or customer company does not exist in this brand, both tenantExternalId and companyExternalId were sent, or another business rule rejects the request.
401unauthorizedThe bearer key is missing, invalid, revoked, or expired; or its machine user is disabled or expired.
403forbiddenThe key does not have the operation's required scope.
403no_brandThe machine user has no brand selected. Select one before creating tickets.
409conflictThe idempotency key was reused with a different payload, a contact belongs to a different company, or another contact uses the external ID.
429rate_limitedThe create limit was reached. Wait at least the number of seconds in the Retry-After response header.
500No code guaranteedThe server could not complete the request. Keep the same idempotency key and payload if retrying a create request.

For a 400, correct the request before retrying. For a 401 or 403, check the key, scopes, and machine user. A deployment that has not configured authentication can return 503; its operator needs to complete setup.

A focused public API

The supported versioned surface currently consists of the two ticket operations above. There is no public v1 endpoint for updating a ticket, posting replies, enumerating all history, or managing workflows. Browser session routes used by the NodeHub app are not interchangeable with this bearer-key API.

To connect services inside a workflow, use a verified account in Connections and the supported operations in the Hub.

Need help with your integration?

Explore your workspace with GraphQL

Workspace owners and administrators can open Settings > API explorer to browse the schema and query tickets, customers, workflows, and run summaries in their active workspace and brand.

The explorer uses your signed-in session and its permissions. It is separate from the bearer-key REST API above. Its createSupportAutomation mutation creates a real unpublished workflow draft, so review a mutation before running it.