> ## Documentation Index
> Fetch the complete documentation index at: https://docs.callkaro.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Raise A Ticket When Nobody Is Free

> Run your own Python at handoff time to create a ticket in your own system

A flow reaches **Hand to a human** and nobody on your team is available. Doing
nothing means the visitor waits for a person who is not coming.

Instead, CallKaro runs **your** Python, creates a ticket in **your** system,
tells the visitor its reference, and closes the conversation cleanly so they can
start again later.

***

## Where To Configure It

[**Settings → Chat routing**](https://callkaro.ai/dashboard/settings), or the
same panel from [**Widgets**](https://callkaro.ai/dashboard/widgets). It is set
once for the whole account, not per agent or per widget.

| Field                     | What it is                                                       |
| ------------------------- | ---------------------------------------------------------------- |
| **Ticket code**           | Python that creates the ticket.                                  |
| **Away message**          | Said when nobody is marked as taking chats.                      |
| **Outside-hours message** | Said when someone asks for a person while you are closed.        |
| **Unanswered message**    | Said when a chat was picked up and nobody replied in time.       |
| **No-reply minutes**      | How long before a picked-up chat counts as unanswered. 0 is off. |

Every field in the panel, in order, is on
[Chat routing](/widget/chat/chat-routing).

All three messages can quote anything your code returns, e.g. `{{ticket_id}}`.
Leave one empty and the away message is used for it.

***

## The Three Reasons A Ticket Is Raised

Your code is given a `reason`, so one function can write a sensible ticket for
all three:

| `reason`     | What happened                                                                                         |
| ------------ | ----------------------------------------------------------------------------------------------------- |
| `away`       | Nobody is marked as taking chats.                                                                     |
| `closed`     | The visitor asked for a person outside your [business hours](/widget/chat/appearance#business-hours). |
| `unanswered` | Somebody was assigned the chat and never replied within your no-reply window.                         |

***

## The Contract

Your code is one async function called `run`. Its argument is the conversation:

```python theme={null}
async def run(conversation):
    ...
    return {"ticket_id": "T-1234"}
```

|             |                                                                                                                      |
| ----------- | -------------------------------------------------------------------------------------------------------------------- |
| Must define | a function **named `run`**, taking one argument. The argument name is yours — `conversation`, `variables`, anything. |
| Gets        | every variable the flow collected, plus `sessionId`, `widgetId`, `contactKey` and `reason`                           |
| Returns     | a dictionary — merged into the conversation's variables                                                              |
| Time limit  | **8 seconds**                                                                                                        |

Whatever you return is available to the away message as `{{...}}`, is stored on
the conversation, and shows up beside the transcript in Contacts. Returning the
ticket id is worth it for that alone — otherwise the reference exists only in
your other system.

***

## The Smallest One That Works

If you only want to see the wiring, start here:

```python theme={null}
async def run(conversation):
    return {"ticket_id": "T-1234"}
```

Set your away message to *"We've raised ticket `{{ticket_id}}`"* and a visitor
asking for a person out of hours reads *"We've raised ticket T-1234"*. Replace
the body with a real API call once you can see it working.

***

## What You Can Return, And Where It Goes

**Return any dictionary you like.** Every key in it becomes a variable on the
conversation, which means it is available in four places at once:

| Where                               | How it appears                                         |
| ----------------------------------- | ------------------------------------------------------ |
| The message the visitor reads       | `{{ticket_id}}`, `{{sla}}`, anything                   |
| The rest of the flow                | as an ordinary variable, if the conversation continues |
| **Contacts**, beside the transcript | listed with everything else the flow collected         |
| The timeline                        | a **ticket created** event, with what you returned     |

```python theme={null}
return {
    "ticket_id":     "T-1234",
    "ticket_url":    "https://desk.you.com/t/1234",
    "queue":         "billing",
    "sla_hours":     "24",
}
```

> We've raised ticket **`{{ticket_id}}`** with our `{{queue}}` team and will
> reply within `{{sla_hours}}` hours.

Only scalars survive — strings, numbers and booleans. Up to **40 keys**, names
truncated to 64 characters and values to 500. A nested object is dropped, so
flatten anything you want to keep.

<Warning>
  Returning the ticket id is worth it even if your message never quotes it.
  Without it the reference exists only in your other system, and nobody opening
  that conversation in Contacts can connect the two.
</Warning>

***

## A Real Example

Creating a ticket in Freshdesk, and handing the id back:

```python theme={null}
import httpx

async def run(conversation):
    payload = {
        "subject": f"Chat handoff — {conversation.get('issue', 'general')}",
        "description": (
            f"Nobody was available ({conversation.get('reason')}).\n\n"
            f"Session: {conversation.get('sessionId')}\n"
            f"Contact: {conversation.get('contactKey')}\n"
            f"Order:   {conversation.get('order_id', '—')}\n"
        ),
        # `or`, not a .get default: an Ask card the visitor skipped leaves the
        # key PRESENT and empty, and a default never fires for that.
        "email": (conversation.get("email") or "").strip() or "unknown@example.com",
        "priority": 2,
        "status": 2,
    }

    async with httpx.AsyncClient(timeout=6) as client:
        r = await client.post(
            "https://yourcompany.freshdesk.com/api/v2/tickets",
            json=payload,
            auth=("YOUR_API_KEY", "X"),
        )
        r.raise_for_status()
        return {"ticket_id": str(r.json()["id"])}
```

With an away message of:

> Nobody is available right now. We have raised ticket **`{{ticket_id}}`** and
> will get back to you.

<Tip>
  Keep your own HTTP timeout below the 8-second limit — 5 or 6 seconds — so a slow
  API returns a clear failure rather than being cut off mid-request.
</Tip>

***

## When Your Code Fails

If the code raises, times out, or you have not written any, the visitor is still
told that someone will come back to them.

Which wording they get depends on **your** wording:

* If your message **quotes a ticket placeholder** (`{{ticket_id}}` and the
  like), it is replaced with a generic sentence — promising a reference that
  does not exist would render as *"ticket  is open"*.
* If it does **not** quote one, your own words are used, because they are still
  true. An account that wrote *"Nobody is available right now, we will email
  you"* should not lose that sentence because an integration was down.

Either way the conversation is recorded, and a **ticket failed** event is
written to the timeline in Contacts — so the failure is visible to your team
rather than only in a log.

<Note>
  Test with the **Run code** card on a canvas first — same runner, same contract,
  same 8-second limit, but you can see the result immediately in **Try it**.
</Note>

***

## What Gets Passed In

```python theme={null}
{
  # everything your flow collected
  "order_id": "ORD12345",
  "issue": "payment",
  "email": "someone@example.com",

  # always present
  "sessionId":  "…",        # this conversation
  "widgetId":   "wgt_…",
  "contactKey": "…",        # the person, stable across conversations
  "reason":     "away",     # or "closed" or "unanswered"
}
```

`contactKey` is the one to store on your ticket. It identifies the person rather
than the conversation, so when they come back you can match the ticket to them.

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Human handoff" icon="user-headset" href="/widget/chat/human-handoff">
    Availability, assignment and capacity
  </Card>

  <Card title="Card types" icon="diagram-project" href="/widget/chat/card-types">
    The Run code card
  </Card>
</CardGroup>
