Let end users file support tickets directly from your own product. Server-to-server, enterprise-only, unifies with widget chats.
Overview
The Tickets API lets your end users file support tickets directly from inside your own product — your billing dashboard, your settings page, your mobile app — without going through the embedded chat widget. Tickets created this way show up in the same Next Cap inbox as widget tickets, fire the same webhooks, and respect the same departments, SLAs, and routing rules.
Enterprise plan only. The Tickets API is sold as a discrete enterprise feature. Tickets created through this path are not counted against your monthly ticket cap.
Update — May 2026: Reply attachments. If your integration uploads files via POST /external/v1/tickets/:id/attachments and expects them to appear next to a customer message, you now need to follow up with POST /external/v1/tickets/:id/replies and pass the new attachment_ids: […] field. Files uploaded without that follow-up still belong to the ticket but won't sit next to a reply in the agent dashboard. Equivalently, on the read side, agent attachments are exposed under events[].attachments in the GET ticket response — make sure your customer-facing UI reads and renders that array. See the Endpoints section below for the full two-step example.
Widget only
Visitors file from the chat widget. Default behavior — no integration work needed.
API only
Tickets are created from your own product's UI via your backend. The chat widget can be hidden or absent.
Both
Either path works. Tickets from the same end user merge into one history regardless of where they were filed.
How It Works
Server-to-server only. Your backend holds an nextcap_tk_ key and calls our REST API on behalf of your authenticated end user. Requests sent directly from a browser are rejected — keep the key on the server.
Each request identifies the end user with a stable external_user_id (your internal user id) plus their email.
Tickets created here are tagged with that identifier so when the same user later files via the chat widget (or vice versa), both tickets appear in their unified history.
An optional HMAC identity hash lets you prove that the body really came from your backend on behalf of that user — useful as a defence-in-depth when end users can also reach the API via your own backend proxy.
Per-widget toggles control which surfaces accept ticket creation: chat widget, this API, or both.
Authentication
Tickets API keys use the branded nextcap_tk_ prefix and are org-level — not tied to a dashboard user, so revoking a teammate doesn't kill your integration.
HTTP Headerbash
Authorization: Bearer nextcap_tk_YOUR_KEY
# or, equivalently:
X-Api-Key: nextcap_tk_YOUR_KEY
Server-to-server only. Any request that arrives with an Origin header is rejected with 403. Never embed this key in a browser bundle, mobile app, or anywhere a visitor can read it.
Generating a Key
Tickets API keys are minted from your Next Cap dashboard — there is no public endpoint to mint one. Sign in as an Owner or Admin and follow the steps below.
Click Create key, give it a descriptive name (e.g. “Production backend”), and submit.
The dashboard reveals two values exactly once. Copy them immediately.
Store them as environment variables on your backend, e.g. NEXTCAP_TICKETS_KEY and NEXTCAP_TICKETS_SECRET.
The two values you receive:
rawKey
nextcap_tk_… — pass as the bearer token on every request.
rawSecret
64-char hex string — used to sign optional HMAC identity hashes. Stored encrypted on our side; you cannot retrieve it later.
Both rawKey and rawSecret are returned exactly once. If you lose them, revoke the key from the dashboard and create a new one. To rotate, create the new key first, deploy it, then revoke the old one.
Per-Widget Enable
Each widget has two independent toggles for ticket creation: via Widget and via API. Both default to off for the API path. Enable per widget in the agent builder Features panel — the toggle is gated to the Enterprise plan. The API rejects ticket creation for any agent_id with API creation disabled (403 api_creation_disabled_for_widget).
Identity Verification (Optional)
By default the API key alone is the authority — anyone holding it can act on behalf of any end user. If you want stricter trust (e.g. you proxy requests on behalf of authenticated users and want a defence-in-depth that the body wasn't tampered with), include an identity_hash field on end_user:
When present and valid, the request is treated as identity-verified. When present and invalid, the request is rejected with a 403. Omitting the field is fine — the API key still authenticates the call.
Quick Start
// In your backend (Node / Next.js API route / Express handler)
const res =awaitfetch("https://api.nextcap.ai/api/v1/external/v1/tickets",{
Each department can carry a list of intake questions that the end user must answer when they file a ticket — account number, error code, screenshot link, etc. Configure them in the dashboard under Tickets → Departments → (pick a widget) → (pick a department) → Intake questions. Each question has a type (short_text, long_text, number, email, url, select, multi_select, date, checkbox), an optional helper URL, and a required flag.
Tree inheritance. Questions can attach to any node in the department tree, not only leaves. The effective set for a picked department is the union of every question on it and every ancestor — root first, leaf last.
Snapshot on submit. When you POST a ticket with intake_answers, we store a snapshot on the ticket: the question id, label, type, and value. Later edits or deletions of the source question never rewrite the answer, so historical tickets remain readable.
Validation. Required questions block submission. Each value is checked per type — email regex, URL shape, select-option membership, etc. Failures return 400 with a structured details payload pointing at the offending question.
Workflow your portal should follow:
Call GET /external/v1/departments?agent_id=…&tree=true. Each node carries a questions[] array of the questions defined directly on it.
Render the tree as a drill-down picker. Once the user reaches a node they want to file under, accumulate its questions plus every ancestor's.
Render the appropriate input for each question. Submit with intake_answers: [{ question_id, value }] on POST /external/v1/tickets.
POST /external/v1/tickets — request bodyjson
{
"agent_id":"uuid",
"subject":"Refund for invoice #4421",
"description":"Charged twice for the same invoice.",
GET /external/v1/tickets/:id — response (excerpt)json
{
"data":{
"id":"...",
"subject":"Refund for invoice #4421",
"department_id":"dept_billing_refunds_uuid",
"intake_answers":[
{
"question_id":"q_account_number",
"label":"Account number",
"type":"short_text",
"value":"ACME-9981",
"required":true
},
{
"question_id":"q_invoice_id",
"label":"Invoice ID",
"type":"short_text",
"value":"INV-4421",
"required":true
},
{
"question_id":"q_priority",
"label":"How urgent is this?",
"type":"select",
"value":"high",
"required":false,
"helper_url":"https://help.acme.com/sla"
},
{
"question_id":"q_screenshot_url",
"label":"Screenshot URL",
"type":"url",
"value":"https://files.acme.com/abc123.png",
"required":false
}
]
}
}
Endpoints
Webhook Events
Tickets created via this API fire the same webhooks as widget-created tickets — your existing CRM integration picks them up automatically. Inspect data.source === "api" and data.external_user_id if you need to branch on origin.
ticket.created — fires when a ticket is filed via this API.
ticket.customer_reply — fires on POST /tickets/:id/replies. Payload now carries attachment_ids: string[] when the reply linked any uploads (omitted when empty).
All other ticket events (ticket.assigned, ticket.status_changed, etc.) fire normally when your team acts on the ticket from the dashboard.
See Webhooks for signing, retries, and the full payload shape.
Cross-Channel Unification
When the same end user files a ticket via the API and chats with the widget, both interactions appear under one history. The match rules are simple:
Same external user id wins. If your widget calls nextcap.identify({ userId, userHash }) with the same id you pass to the API, the two surfaces share one history immediately.
Email link. If the widget visitor previously gave an email that matches your API call's end_user.email, the prior chats become linked to that user the first time you submit via the API.
New contact. If neither matches, the user is treated as a brand-new contact in your organization.
For the tightest unification, also enable widget identity verification and pass the same userId to both nextcap.identify() and the API's external_user_id. Email-only matching covers the common case but can't link genuinely anonymous visitors.
Rate Limits
Tickets API keys default to 600 requests per minute, generous enough that flooding protection kicks in well before you notice. Per-key limits are configurable on request. Standard X-RateLimit-Limit, X-RateLimit-Remaining, and Retry-After headers are returned — see Errors & Rate Limits.
Common Errors
401 — the API key is invalid, revoked, or expired. Generate a new nextcap_tk_ key from the dashboard.
403 — the request was sent with a browser Origin header. Move the call to your backend.
403 — your plan does not include the Tickets API. Upgrade to Enterprise or contact sales.
403 api_creation_disabled_for_widget — the agent_id you sent has the Tickets API toggle off. Enable it in the widget's Features panel.
403 — the identity_hash didn't verify against your signing secret. Re-check the input format ($${external_user_id}:${email}) and that you used the right secret.
400 — the department_id isn't one this widget can route to. Pick a department visible to the widget or omit the field.
400 missing_required_answer — one of the department's required intake questions wasn't in intake_answers. The response details.question_id tells you which one.
400 invalid_answer_value — an answer's value didn't pass type validation (bad email, value not in select options, number out of range, etc.). The details payload includes the question_id and a reason code.
400 unknown_question_id — intake_answers contained an id that isn't in the chosen department's effective question set. Refetch /external/v1/departments?tree=true to pick up renames or deletions.
404 — ticket not found. Either the id is wrong or it doesn't belong to the end user you provided.
429 — you exceeded the rate limit. Back off using the Retry-After header.
Need real-time push of ticket events into your own product? Combine this API with Webhooks — fire updates back into the customer portal the moment your support team responds.