GeniffyDocs
Changelog Log In Get a key

Zendesk

Give each of your customers a memory of their support history, so your assistant or your agents start from what happened last time. This sync reads your Zendesk tickets and adds each one to its requester's own memory, with every public reply, who wrote it and when. Internal notes stay out, so nothing an agent wrote for colleagues reaches the customer through your assistant. It reads through Zendesk's incremental export: the first run reads every ticket since a time you choose, and each run after it only the tickets that changed, including the ones deleted.

Install

Terminal
pip install httpx geniffy

Set GENIFFY_API_KEY from API keys in the Geniffy app. In Zendesk's Admin Center, under Apps and integrations, make an API token, and use it with the email address of an admin.

The sync

zendesk_sync.py
import time

import httpx
from geniffy import Geniffy, NotFoundError

geniffy = Geniffy()                               # reads GENIFFY_API_KEY
LABELS = {"channel": "zendesk"}


def zendesk(subdomain: str, email: str, api_token: str) -> httpx.Client:
    return httpx.Client(base_url=f"https://{subdomain}.zendesk.com/api/v2", timeout=60,
                        auth=(f"{email}/token", api_token))


def space_for(ticket: dict) -> str:
    """The customer who asked, by their Zendesk user id. If you know them by your own id, use that."""
    return f"customer_{ticket['requester_id']}"


def thread(api: httpx.Client, ticket: dict) -> str:
    """A ticket as one note: its subject and state, then each public reply with who wrote it and when."""
    parts = [f"Ticket #{ticket['id']}: {ticket['subject']} ({ticket['status']})."]
    url, params = f"/tickets/{ticket['id']}/comments", {"include": "users", "page[size]": 100}
    while url:
        out = api.get(url, params=params).raise_for_status().json()
        names = {user["id"]: user["name"] for user in out.get("users", [])}
        for comment in out["comments"]:
            if comment["public"] and comment["plain_body"].strip():     # internal notes stay out
                who = names.get(comment["author_id"], "someone")
                parts.append(f"From {who}, {comment['created_at'][:10]}:\n{comment['plain_body']}")
        url, params = (out["links"]["next"] if out["meta"]["has_more"] else None), None
    return "\n\n".join(parts)


def sync(subdomain: str, email: str, api_token: str, cursor: str | None = None, start_time: int = 0) -> str:
    """Bring the tickets that changed into each customer's memory. Returns the cursor to pass next time: pass
    None the first time, and every ticket changed since start_time (a Unix time) is read."""
    with zendesk(subdomain, email, api_token) as api:
        params = {"cursor": cursor} if cursor else {"start_time": start_time}
        while True:
            got = api.get("/incremental/tickets/cursor", params=params)
            if got.status_code == 429:            # ten calls a minute to this one: wait as Zendesk asks
                time.sleep(int(got.headers.get("Retry-After", "60")))
                continue
            out = got.raise_for_status().json()
            for ticket in out["tickets"]:
                mem, ref = geniffy.space(space_for(ticket)), f"zendesk:{ticket['id']}"
                if ticket["status"] == "deleted":
                    try:
                        mem.sources.delete(external_id=ref)
                    except NotFoundError:
                        pass
                else:
                    mem.memories.add(thread(api, ticket), title=f"#{ticket['id']} {ticket['subject']}",
                                     said_at=ticket["updated_at"], external_id=ref, labels=LABELS)
            if out["end_of_stream"]:
                return out["after_cursor"]
            params = {"cursor": out["after_cursor"]}

Run it on a schedule, and keep the cursor each run returns for the next: cursor = sync("acme", "admin@acme.com", token, cursor). On the first run, start_time says how far back to read, as a Unix time at least a minute in the past.

Recall from it

When a customer writes in, put what is known about them in front of your model:

known = geniffy.space(f"customer_{requester_id}").context(message)

Each ticket is cited by its number and subject, so an answer can point to it.

How it behaves

  • Each ticket is one source, in its requester's memory, under its number and dated by its last update. A new reply sends the ticket again, and only what it adds is learned. See Your own ids.
  • Only public replies. Internal notes are left out, so your assistant never repeats to a customer what agents wrote for each other.
  • Only what changed is read. After the first run, the cursor names the tickets that changed. A ticket deleted in Zendesk is deleted from memory with what it taught.
  • One memory for each customer, so what one customer said never reaches another. If your app knows them by its own id, change space_for to use it. To forget a customer, erase their space: geniffy.forget_space(f"customer_{requester_id}").
Last updated October 6, 2026