API examples
GVenta Help Desk API requests: create tickets from other systems, list unassigned work, reply with attachments, add notes, the client portal and AI agents.
Documented from GVenta Help Desk version 1.8.55.
This page walks through the most common things integrations do with the GVenta Help Desk API, with a request and a response for each. Read API getting started first for authentication, CSRF and the response envelope.
All requests below go to https://your-instance.example. Responses are illustrative: they show the envelope and the field names you can rely on, but the exact set of fields and their values will vary. Consult the endpoint reference for the definitive list of parameters.
Create a ticket from an external system
The classic integration: something in another system needs a ticket. Authenticate with an API key, which needs no CSRF token. POST /api/tickets accepts either an API key or a JWT.
POST /api/tickets HTTP/1.1
Host: your-instance.example
X-API-Key: 9f2c…
Content-Type: application/json
{
"subject": "Card reader offline at store 42",
"requester_email": "manager@retailco.com",
"requester_name": "Dana Ruiz",
"description": "Terminal 3 shows E-114 since 08:20.",
"priority": "high",
"source": "Store Ops Portal"
}
{
"success": true,
"data": {
"id": 4821,
"ticket_number": "TICK-20260824-0017",
"subject": "Card reader offline at store 42",
"status": "open",
"priority": "high",
"source": "Store Ops Portal",
"requester_email": "manager@retailco.com",
"requester_name": "Dana Ruiz",
"customer_id": 112,
"first_response_due_at": "2026-08-24T11:20:00-04:00",
"sla_due_at": "2026-08-24T13:20:00-04:00",
"created_at": "2026-08-24T09:20:00-04:00"
},
"message": "Ticket created"
}
Only subject and a valid requester_email are required. Optional fields are status, priority, description, source, assigned_to and customer_id. A description becomes the ticket’s opening message, and setting assigned_to sends the assignee a notification email.
The response carries the generated ticket_number, the business-hours SLA deadlines calculated for its priority (see The SLA engine), and the customer the requester resolved to.
About source. The value auto-registers in the ticket-source vocabulary on first use. “Store Ops Portal” immediately becomes a filterable, color-codable channel in the ticket list and in reporting, with no administrator setup. Administrators can later rename or recolor it under Settings → Ticket Sources.
List open and in-progress tickets nobody owns
GET /api/tickets accepts a comma-separated status, and assigned_to accepts the special value unassigned. This is the query a dispatcher or an AI triage agent would run.
GET /api/tickets?status=open,in_progress&assigned_to=unassigned&limit=20 HTTP/1.1
Host: your-instance.example
X-API-Key: 9f2c…
{
"success": true,
"data": [
{
"id": 4821,
"ticket_number": "TICK-20260824-0017",
"subject": "Card reader offline at store 42",
"status": "open",
"priority": "high",
"assigned_to": null,
"customer_id": 112,
"sla_due_at": "2026-08-24T13:20:00-04:00",
"updated_at": "2026-08-24T09:20:00-04:00"
}
],
"meta": {
"total": 7,
"limit": 20,
"offset": 0,
"count": 7,
"filters_applied": {
"status": "open,in_progress",
"assigned_to": "unassigned"
}
}
}
Other useful filters on this endpoint are priority, requester_email, customer_id (comma-separated), source, search, tags, and date_from / date_to with date_field set to created_at, updated_at, resolved_at, closed_at or sla_due_at. Sort with order_by and order_dir. limit is capped at 100.
Reply to a ticket with an attachment
POST /api/tickets/{id}/reply requires a staff JWT, so it also needs a CSRF token. Send multipart/form-data so the body, CC list and files travel in one request. cc is a JSON array, and files go in attachments[].
POST /api/tickets/4821/reply HTTP/1.1
Host: your-instance.example
Authorization: Bearer <access_token>
X-CSRF-Token: <token>
Content-Type: multipart/form-data; boundary=----gv
------gv
Content-Disposition: form-data; name="body"
Hi Dana, a replacement reader ships today. Tracking attached.
------gv
Content-Disposition: form-data; name="internal_note"
0
------gv
Content-Disposition: form-data; name="cc"
["ops-lead@retailco.com"]
------gv
Content-Disposition: form-data; name="attachments[]"; filename="tracking.pdf"
Content-Type: application/pdf
(binary)
------gv--
{
"success": true,
"data": {
"id": 19044,
"message_type": "agent_reply",
"email_sent": true,
"email_error": null,
"email_error_code": null
},
"message": "Reply sent"
}
The application persists the message first, resolves and records the CC addresses, uploads the attachment to private storage, emails the customer with the file attached (not linked), and stamps first_response_at on the ticket if this is the first agent reply.
The three delivery fields are the important part. If the email could not be sent, the reply is still saved and email_sent is false, with email_error and email_error_code telling you why. The application distinguishes sender not configured, not authenticated and incomplete configuration from general delivery failures, so your integration can tell an administrator exactly what to fix. See The email pipeline.
You can also send this endpoint plain JSON with body, is_html, internal_note and cc when there are no files.
Post an internal note
Set internal_note to true on the same endpoint. The note is recorded on the ticket, visually flagged in the staff conversation, never emailed, and never returned to the client portal.
POST /api/tickets/4821/reply HTTP/1.1
Host: your-instance.example
Authorization: Bearer <access_token>
X-CSRF-Token: <token>
Content-Type: application/json
{
"body": "Store 42 has had two reader failures this quarter. Check the power supply before shipping a third.",
"internal_note": true
}
{
"success": true,
"data": {
"id": 19045,
"message_type": "internal_note",
"created_at": "2026-08-24T09:41:00-04:00"
},
"message": "Internal note added"
}
The client portal flow
A customer’s own system can raise tickets on their behalf using the client-scoped endpoints. Sign in first with POST /api/client/auth/login, which returns a client-scoped token pair.
POST /api/client/auth/login HTTP/1.1
Host: your-instance.example
Content-Type: application/json
{ "email": "manager@retailco.com", "password": "…" }
{
"success": true,
"data": {
"access_token": "eyJ…",
"refresh_token": "…"
}
}
Then create the ticket. POST /api/client/tickets takes subject, description, an optional priority and optional attachments[] (as multipart). Because the caller is a JWT rather than an API key, include a CSRF token as described in API getting started.
POST /api/client/tickets HTTP/1.1
Host: your-instance.example
Authorization: Bearer eyJ…
X-CSRF-Token: <token>
Content-Type: application/json
{
"subject": "Receipt printer jams every morning",
"description": "Paper jams on the first print of the day, fine afterwards.",
"priority": "medium"
}
{
"success": true,
"data": {
"id": 4822,
"ticket_number": "TICK-20260824-0018",
"subject": "Receipt printer jams every morning",
"status": "open",
"priority": "medium",
"customer_id": 112
},
"message": "Ticket created"
}
Portal tickets go through exactly the same creation path as email and staff-created tickets, so they get the same numbering, SLA deadlines and notifications. The priority is a suggestion: an unrecognized value falls back to medium rather than failing. Listing with GET /api/client/tickets and reading GET /api/client/tickets/{id} are always filtered to the contact’s own company, and internal notes are removed on the server before the conversation is returned. Replies go to POST /api/client/tickets/{id}/responses. See Client portal and the client portal endpoints.
Read public knowledge base articles
Article listing and reading require no authentication, which makes it straightforward to surface your help content on another site or feed it to a bot. Permission checking still applies, so an unauthenticated caller only sees what public access allows.
GET /api/kb/articles?status=published&search=card%20reader HTTP/1.1
Host: your-instance.example
{
"success": true,
"data": [
{
"id": 57,
"title": "Resetting a card reader that shows E-114",
"slug": "resetting-a-card-reader-e-114",
"excerpt": "Power-cycle the terminal, then hold the function key for five seconds…",
"status": "published",
"visibility": "public",
"category_id": 4,
"is_featured": false,
"published_at": "2026-05-02T14:00:00-04:00"
}
],
"meta": { "total": 1, "limit": 20, "offset": 0, "count": 1 }
}
Fetch a single article by ID with GET /api/kb/articles/{id} or by slug with GET /api/kb/articles/slug/{slug}. Other list filters include category_id, is_featured, tag_id and visibility. Helpfulness votes are also open: POST /api/kb/articles/{id}/vote/helpful and …/vote/not-helpful. See the knowledge base endpoints.
Giving an AI agent a seat
A staff user can be flagged as an AI agent (is_ai_agent) with two independent capability switches, can_reply_to_customer and can_suggest_answers. Set them when creating the user with POST /api/users or under Settings → User Management. AI accounts show a robot badge throughout the interface.
The reply endpoint enforces the switches on the server:
- An AI account without
can_reply_to_customerthat attempts a customer-facing reply is refused with 403. - An AI account with neither switch is refused with 403 even for internal notes.
So an agent granted only can_suggest_answers can draft suggestions as internal notes for a human to review, but can never email a customer, no matter what its prompt says.
{
"success": false,
"error": "FORBIDDEN",
"message": "AI agent accounts without can_reply_to_customer cannot send customer-facing replies"
}
(The error code and message above are illustrative.) An AI agent authenticates like any other staff user, with a JWT or an API token created for its account, and everything it does is written to the audit log under its own identity.
Why it matters: You can safely give an AI assistant a seat, drafting internal suggestions only or replying to customers directly, with the boundary enforced by the server, not by trust.