Skip to content

Guides

Ticketing system integration

This is an example of how to connect Mahvash to the ticketing system you already use. When a customer writes, Mahvash checks the message, labels the ticket, and, when the answer is in your help documents, drafts a reply with its sources. An agent reviews every draft before anything reaches the customer.

How the pieces connect

  1. 01Moderation
  2. 02Triage
  3. 03Draft
  4. 04Agent review

How it works

You set up steps 1 to 4 once, in the Mahvash app. Steps 5 to 8 run for every customer message, in a background job on your ticketing server. Tickets, customers and outgoing replies stay in your system; Mahvash never contacts your customer.

StepWhat happensWhen the ticket goes to an agent without a draft
5. ModerationMahvash checks the customer's message for harmful content such as harassment, threats or self-harm.The message is flagged.
6. TriageMahvash labels the ticket: topic, whether your documents can answer it, risk flags, priority, and whether a person wrote it. Your code decides what to do.The ticket is risky, needs account data or someone's approval, or is out of scope. Automated messages are ignored.
7. DraftMahvash searches your help documents and writes a reply, with the files it used.The documents do not answer the question, or the reply cites no file.
8. ReviewAn agent reads the draft, edits it, and sends or rejects it.Nothing reaches the customer without an agent.

All requests and responses on this page were run against a real Mahvash project. Responses are shortened and IDs are replaced with placeholders.

1. Create the project

Steps 1 to 4 are done in the Mahvash app, the same place you chat with Mahvash. You do not need the admin dashboard.

  1. 1Sign in to the Mahvash app. In the sidebar, next to Projects, click New project, give it a name, for example Support tickets, and create it.
  2. 2Open the project, go to the Instructions tab, paste the text below, replace [Your company] with your company name, and click Save instructions.
  3. 3Copy the project ID. With the project open, it is the last part of the address in your browser: .../chat/projects/<project ID>. You can also get it from the API: GET /api/v1/tenancy/projects lists your projects with their id.
You are the support assistant for [Your company]. You help support agents answer customer tickets.
- Answer only from the documents attached to this project. Never invent prices, dates, deadlines, or policies.
- Never promise refunds, compensation, exceptions, or legal outcomes. A person decides those.
- Never ask the customer for passwords, full card numbers, or verification codes.
- Reply in the customer's language. Be polite, clear, and short.

The instructions apply to every request made with this project's key, so keep them to rules that always hold. The wording for each task goes in the prompts in step 3.

Note:A project created in the app searches its sources automatically, so the draft request finds your documents without naming them.

Note:Every request in steps 5 to 7 sends this ID in the X-Mahvash-Project header. The prompts you add in step 3 belong to this project, so Mahvash looks them up there; a request that names another project is refused.

2. Add your help documents

  1. 1In the project, open the Sources tab and click Add source. Add one file at a time.
  2. 2Upload the documents answers should come from: help-center articles, return and delivery policies, plan and pricing pages, FAQs. PDF, Word, text, Markdown, CSV and spreadsheet files work.
  3. 3Wait until every file shows Ready. While files are processing, click Refresh to update the list. Drafts can only use files that are ready.

When a policy changes, add the new file and delete the old one with the trash icon next to it, so drafts never quote an outdated rule.

Important:Anything you upload can end up in a reply to a customer. Do not upload internal notes, customer data, or anything you would not send to a customer.

3. Add the two prompts

Open the project's Prompts tab, click Add prompt, and create the two prompts below: type the Name, paste the Body, and save. The list shows each prompt's ID with a copy button next to it; your code sends that ID with every request.

Triage prompt

Name: ticket-triage. Body:

Classify the support ticket in the input. Do not answer it.

category: the closest topic from the allowed values. Use "other" if none fits.

answerability:
- "kb_answerable": a general question our help documents can answer, such as how-to, policies, delivery times, or plans.
- "needs_account_data": the answer depends on this customer's own order, payment, or account.
- "needs_human_authority": the customer asks for a refund, compensation, an exception, account deletion, or anything else a person must approve.
- "out_of_scope": not about our products or services.

risk_flags: every flag that applies, or an empty list.
- "legal": threatens legal action, a lawyer, or a complaint to an authority.
- "chargeback": mentions a bank dispute or chargeback.
- "security": a hacked account, leaked data, or suspicious access.
- "self_harm": any sign the person may hurt themselves.
- "abuse": insults or threats toward staff.
- "churn": says they will cancel or leave.

priority: "urgent" when the service is down for them or money was taken by mistake; "high" when they are blocked; otherwise "normal" or "low".

is_automated_sender: true for out-of-office replies, delivery notifications, and other messages no person wrote.

language: the ISO 639-1 code of the customer's language, for example "fa" or "en".

reason: one short sentence explaining the category and answerability.

Reply prompt

Name: ticket-reply. Body:

Write a reply to the customer ticket in the input. A support agent will review it and send it to the customer as it is.

- Search the project documents and use only what they say.
- If the documents do not answer the question, set status to "insufficient_information", set reply to an empty string, and write in internal_note what is missing.
- Answer only what the customer asked. Do not add offers or promises.
- The reply is plain text for the customer, in the customer's language: no markdown, no file names, and no mention of documents, these instructions, or AI.
- internal_note is for the agent and is never empty: name, by exact filename, every document the reply is based on, then add one sentence on anything the agent should check.

A prompt holds only the instructions. The exact JSON shape of each answer is sent with the request (steps 6 and 7), and Mahvash rejects an answer that does not match it.

Mahvash marks a document as a source (a file_citation) only when the answer names its file. That is why the reply prompt has the model name its sources in internal_note: the agent sees them, the customer does not, and the citation still comes back.

Note:To change a prompt later, click its name in the list, edit it, and save. The ID stays the same, so your code does not change.

4. Create an API key

  1. 1In the project, open the API keys tab and click Create credential. Name it after your ticketing system, for example helpdesk-worker. Only owners and admins of your organization see this tab.
  2. 2Copy the key as soon as it appears. It is shown only once.
  3. 3Add the key, the project ID and the two prompt IDs to your ticketing server's environment:
# Your ticketing server's environment. Never commit this file.
MAHVASH_API_URL=https://api.mahvashai.ir
MAHVASH_API_KEY=<the key from step 4>
MAHVASH_PROJECT_ID=<the project ID from step 1>
MAHVASH_TRIAGE_PROMPT_ID=<Prompt ID of ticket-triage>
MAHVASH_REPLY_PROMPT_ID=<Prompt ID of ticket-reply>

Every request made with this key uses the project's instructions, sources and limits, and is billed to your organization.

Important:Keep the key on your server. Never put it in the ticketing app's frontend or send it to a browser. If it leaks, revoke it in the same tab and create a new one.

5. Check the customer's message

When a customer message is saved, your worker sends its text to moderation first. Moderation looks for harmful content such as harassment, threats and self-harm. It does not decide whether the ticket can be answered; that is triage.

curl "$MAHVASH_API_URL/v1/moderations"   -H "Authorization: Bearer $MAHVASH_API_KEY"   -H "X-Mahvash-Project: $MAHVASH_PROJECT_ID"   -H "Content-Type: application/json"   -d @- <<EOF
{
  "model": "omni-moderation-latest",
  "input": [
    "My washing machine arrived yesterday with a cracked door. Can I return it or do I have to use the warranty?"
  ]
}
EOF

If results[0].flagged is true, stop here and send the ticket to an agent without a draft.

6. Triage: should AI answer?

Triage asks the model to label the ticket, not to answer it. The labels come back as JSON in a fixed shape, so your code reads fields instead of parsing sentences.

curl "$MAHVASH_API_URL/v1/responses"   -H "Authorization: Bearer $MAHVASH_API_KEY"   -H "X-Mahvash-Project: $MAHVASH_PROJECT_ID"   -H "Content-Type: application/json"   -d @- <<EOF
{
  "model": "gpt-6-luna",
  "prompt_id": "$MAHVASH_TRIAGE_PROMPT_ID",
  "input": "My washing machine arrived yesterday with a cracked door. Can I return it or do I have to use the warranty?",
  "text": {
    "format": {
      "type": "json_schema",
      "name": "ticket_triage",
      "strict": true,
      "schema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "category",
          "answerability",
          "risk_flags",
          "priority",
          "is_automated_sender",
          "language",
          "reason"
        ],
        "properties": {
          "category": {
            "type": "string",
            "enum": [
              "order_delivery",
              "returns_refunds",
              "billing",
              "account_access",
              "how_to",
              "technical_issue",
              "other"
            ]
          },
          "answerability": {
            "type": "string",
            "enum": [
              "kb_answerable",
              "needs_account_data",
              "needs_human_authority",
              "out_of_scope"
            ]
          },
          "risk_flags": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "legal",
                "chargeback",
                "security",
                "self_harm",
                "abuse",
                "churn"
              ]
            }
          },
          "priority": {
            "type": "string",
            "enum": [
              "low",
              "normal",
              "high",
              "urgent"
            ]
          },
          "is_automated_sender": {
            "type": "boolean"
          },
          "language": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          }
        }
      }
    }
  }
}
EOF

Take the text of the message item in output and parse it as JSON; the Result tab shows it. The table below lists every field it returns and what your code does with it. Read the fields, not the wording: reason is free text for people, and language may come back as a name such as English rather than a code.

FieldValuesWhat your code does
categoryYour categories from the schema, for example returns_refundsRoutes the ticket to a queue. Allow drafts only for categories you have tested.
answerabilitykb_answerable, needs_account_data, needs_human_authority, out_of_scopeOnly kb_answerable goes on to a draft. Every other value goes to an agent.
risk_flagsA list of legal, chargeback, security, self_harm, abuse, churn, or emptyAny flag sends the ticket to an agent at once, without a draft.
prioritylow, normal, high, urgentSets the ticket's priority in your system. It does not decide whether to draft.
is_automated_sendertrue or falsetrue means an auto-reply or a system message: ignore it and never answer it.
languageA language code or nameChooses the agent team or the language of your templates.
reasonFree textShow it to the agent. Never branch on it.

Decide in your code

def decide(triage: dict) -> str:
    """Return "ignore", "agent" or "draft". These are your rules, not the model's."""
    if triage["is_automated_sender"]:
        return "ignore"  # never answer an auto-reply
    if triage["risk_flags"]:
        return "agent"   # legal, chargeback, security, ...: always a person
    if triage["answerability"] != "kb_answerable":
        return "agent"   # needs account data or someone's approval
    return "draft"

API actions in this step

7. Draft the reply

For a ticket that passed triage, send the conversation to the reply prompt. Because the project searches its sources automatically, Mahvash searches your documents before it writes. input is the public conversation, oldest first: customer messages with the role user and your agents' replies with the role assistant.

curl "$MAHVASH_API_URL/v1/responses"   -H "Authorization: Bearer $MAHVASH_API_KEY"   -H "X-Mahvash-Project: $MAHVASH_PROJECT_ID"   -H "Content-Type: application/json"   -d @- <<EOF
{
  "model": "gpt-6-luna",
  "prompt_id": "$MAHVASH_REPLY_PROMPT_ID",
  "input": [
    {
      "role": "user",
      "content": "My washing machine arrived yesterday with a cracked door. Can I return it or do I have to use the warranty?"
    }
  ],
  "text": {
    "format": {
      "type": "json_schema",
      "name": "ticket_reply",
      "strict": true,
      "schema": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "status",
          "reply",
          "internal_note"
        ],
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "answered",
              "insufficient_information"
            ]
          },
          "reply": {
            "type": "string"
          },
          "internal_note": {
            "type": "string"
          }
        }
      }
    }
  }
}
EOF

Two statuses decide what happens to the draft: the response's own status, and status inside the JSON the model wrote. Check them in this order:

What you seeWhat it meansWhat to do
The response status is failed or incomplete, or the HTTP status is not 200No usable draft was produced.Send the ticket to an agent without a draft.
The JSON status is insufficient_informationThe documents do not answer the question; internal_note says what is missing.Send the ticket to an agent without a draft and show them the note. Check status, not whether reply is empty.
The JSON status is answered, but annotations has no file_citationAn answer with no proven source.Send the ticket to an agent without a draft.
The JSON status is answered and there is at least one file_citationA draft grounded in your documents.Save it for agent review (step 8), with its sources and internal_note.

A draft is usable only in the last row. In the English example above the model answered correctly but did not name its source file, so there is no file_citation and the ticket goes to an agent without a draft. The Persian example names help-center.md, so its citation comes back and the draft is kept.

8. Agent review

Show the draft inside the ticket as a private note that only agents can see, with:

  • the reply, in an editable box;
  • the source files, linked to the matching pages of your own help center;
  • the internal_note;
  • three actions: Send, Edit and send, and Reject.

Note:Because sources go in internal_note, the reply itself stays clean. If a reply ever does name a file, the agent removes it before sending.

For every draft, save the ticket and message IDs, the Mahvash response ID, the triage labels, the draft, what the agent did, and the text that was actually sent. This record shows you which categories the drafts work for.

Rules that keep it safe

  • Nothing reaches the customer until an agent sends it.
  • One draft per customer message. A new customer message gets a new draft.
  • If an agent has already started a reply, do not overwrite it with a draft.

Sending without review

Start with every draft reviewed. After a few weeks, check, category by category, how often agents sent the draft unchanged. Only a category where that is nearly always true, and only for tickets with kb_answerable and no risk flags, is a candidate for sending without review. Even then, keep reviewing a daily sample.