Vai al contenuto
[ BLOG / AGENTI AI ]

Integrating GPT into TeamSystem without replacing the ERP

API-side integration patterns to pair an LLM with TeamSystem without touching the core: webhooks, Italian prompts, data security. Ready-to-adapt TypeScript code.

Andrea Barberi 11 min

Integrating an LLM with an Italian business management system like TeamSystem runs into one fact: the system isn’t really yours. It’s a closed product, with partial APIs, internal certification processes, and any modification to its core breaks the support guarantee and automatic updates. That’s why the strategy that works is to pair the business management system with an AI layer that reads and writes it via API, without touching its code. Let’s see how, with adaptable TypeScript code.

What you’ll have at the end

A Node.js service that:

  1. Receives an event from TeamSystem (new client record, ticket opened, email received) via webhook or polling
  2. Retrieves the necessary context data via TeamSystem API
  3. Calls an LLM (Claude or GPT) with a contextualized Italian prompt
  4. Writes the result as a note, activity, or reminder in TeamSystem

The business management system stays intact. The agent lives in a separate service, with its own logs, its own rate limits, its own credentials. If the agent breaks, TeamSystem keeps working normally.

Prerequisites

  • Node.js 22+ and pnpm/npm
  • Administrative access to TeamSystem (to generate API credentials)
  • OpenAI or Anthropic key (in .env, never hardcoded)
  • A public HTTPS endpoint for webhooks (in dev: ngrok or Cloudflare tunneling; in prod: Vercel/Fly/your own server)

A preliminary check that makes a difference: ask your TeamSystem contact which REST APIs are active on your installation. Enterprise and Cloud editions expose documented REST APIs. Older on-premise editions sometimes only have SOAP or direct database access. The integration strategy changes drastically.

Step 1: configure the project

mkdir teamsystem-llm-bridge && cd teamsystem-llm-bridge
pnpm init
pnpm add express zod openai dotenv
pnpm add -D typescript tsx @types/express @types/node
npx tsc --init --target es2022 --module nodenext --moduleResolution nodenext

Minimal structure:

teamsystem-llm-bridge/
├── src/
│   ├── index.ts            # express server
│   ├── teamsystem.ts       # TeamSystem API client
│   ├── llm.ts              # LLM client
│   └── handlers/
│       └── ticket-opened.ts
├── .env                    # credentials, NOT in git
└── package.json

Step 2: TeamSystem API client

The standard pattern is a typed client with zod to validate responses. Example to retrieve a ticket:

// src/teamsystem.ts
import { z } from 'zod'

const TICKET_SCHEMA = z.object({
  id: z.string(),
  oggetto: z.string(),
  descrizione: z.string(),
  cliente: z.object({
    ragioneSociale: z.string(),
    piva: z.string().optional(),
  }),
  prioritaCodice: z.enum(['BASSA', 'MEDIA', 'ALTA', 'URGENTE']),
  dataApertura: z.coerce.date(),
})

export type Ticket = z.infer<typeof TICKET_SCHEMA>

const BASE_URL = process.env.TEAMSYSTEM_API_URL!
const TOKEN = process.env.TEAMSYSTEM_API_TOKEN!

export async function getTicket(ticketId: string): Promise<Ticket> {
  const res = await fetch(`${BASE_URL}/api/v1/tickets/${ticketId}`, {
    headers: { Authorization: `Bearer ${TOKEN}` },
  })
  if (!res.ok) throw new Error(`TeamSystem getTicket failed: ${res.status}`)
  return TICKET_SCHEMA.parse(await res.json())
}

export async function addNoteToTicket(
  ticketId: string,
  noteBody: string,
): Promise<void> {
  const res = await fetch(`${BASE_URL}/api/v1/tickets/${ticketId}/notes`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ corpo: noteBody, tipo: 'INTERNA' }),
  })
  if (!res.ok) throw new Error(`TeamSystem addNote failed: ${res.status}`)
}

Critical point: zod validation is not redundant. Italian business management system APIs have historically changed fields without notice between releases. Validating on input lets your service break in a controlled way (with clear logs) instead of propagating malformed data to the LLM.

Step 3: LLM client with native Italian prompt

The single most frequent mistake in LLM integration with Italian business management systems is writing the prompt in English thinking “the model will translate anyway”. Models are significantly more accurate when output and prompt are in the same language. We write natively in Italian.

// src/llm.ts
import OpenAI from 'openai'

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })

export async function suggestTicketResponse(
  ticketDescription: string,
  clientName: string,
): Promise<string> {
  const completion = await client.chat.completions.create({
    model: 'gpt-4o-mini',
    temperature: 0.3,
    max_tokens: 400,
    messages: [
      {
        role: 'system',
        content: `Sei un assistente che supporta operatori italiani del customer service.
Il tuo compito: dato il testo di un ticket aperto da un cliente, proponi una bozza di risposta in italiano formale, professionale, concisa (max 8 righe).

Regole vincolanti:
- Italiano corretto, senza anglicismi superflui
- Mai promesse di tempi o costi specifici (decide l'operatore)
- Se la richiesta non è chiara, suggerisci 1-2 domande di chiarimento invece di una risposta
- Apri con "Gentile [nome cliente]" e chiudi con "Cordiali saluti"
- Non aggiungere firme o nominativi del nostro team`,
      },
      {
        role: 'user',
        content: `Cliente: ${clientName}\n\nTesto del ticket:\n${ticketDescription}`,
      },
    ],
  })

  return completion.choices[0]?.message?.content?.trim() ?? ''
}

Three details worth the ink:

  1. temperature: 0.3 for deterministic professional output. Higher introduces unwanted variability in business context.
  2. max_tokens: 400 as a cost guardrail. Above this is rarely needed; below risks cutting off responses.
  3. System prompt in native Italian, with explicit binding rules. Never say “in italian style” or “professionally” in English: write the rules in the language you want to see in output.

Step 4: webhook handler

// src/handlers/ticket-opened.ts
import { getTicket, addNoteToTicket } from '../teamsystem'
import { suggestTicketResponse } from '../llm'

export async function handleTicketOpened(ticketId: string): Promise<void> {
  const ticket = await getTicket(ticketId)

  // Early filters: don't waste tokens on irrelevant tickets
  if (ticket.prioritaCodice === 'URGENTE') {
    // Urgent tickets must be handled by an operator without AI intermediation
    return
  }
  if (ticket.descrizione.length < 30) {
    // Description too short: likely spam or mistype
    return
  }

  const suggestion = await suggestTicketResponse(
    ticket.descrizione,
    ticket.cliente.ragioneSociale,
  )

  await addNoteToTicket(
    ticket.id,
    `[Suggerimento AI generato il ${new Date().toISOString()}]\n\n${suggestion}\n\n---\nQuesto è un suggerimento. Verificare e modificare prima di inviare al cliente.`,
  )
}

Note the “early filter”: don’t call the LLM on every ticket. Explicit exclusion rules (urgent priorities, descriptions too short, specific sensitive clients) reduce cost by 30-50% and improve the signal-to-noise ratio of suggestions.

Step 5: Express server with webhook signature verification

// src/index.ts
import express from 'express'
import crypto from 'node:crypto'
import { z } from 'zod'
import { handleTicketOpened } from './handlers/ticket-opened'

const app = express()
app.use(express.json({ verify: storeRawBody }))

const WEBHOOK_SECRET = process.env.TEAMSYSTEM_WEBHOOK_SECRET!

function storeRawBody(
  req: express.Request,
  _res: express.Response,
  buf: Buffer,
): void {
  ;(req as express.Request & { rawBody?: Buffer }).rawBody = buf
}

function verifySignature(req: express.Request): boolean {
  const signature = req.header('X-TeamSystem-Signature')
  if (!signature) return false
  const rawBody = (req as express.Request & { rawBody?: Buffer }).rawBody
  if (!rawBody) return false
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex')
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
}

const WEBHOOK_PAYLOAD = z.object({
  evento: z.literal('ticket.aperto'),
  ticketId: z.string(),
})

app.post('/webhook/teamsystem', async (req, res) => {
  if (!verifySignature(req)) {
    res.status(401).json({ error: 'firma non valida' })
    return
  }

  const parsed = WEBHOOK_PAYLOAD.safeParse(req.body)
  if (!parsed.success) {
    res.status(400).json({ error: 'payload non valido' })
    return
  }

  res.status(202).json({ status: 'in-elaborazione' })
  handleTicketOpened(parsed.data.ticketId).catch((err) => {
    console.error('handleTicketOpened error:', err)
  })
})

const PORT = Number(process.env.PORT ?? 3000)
app.listen(PORT, () => console.log(`bridge listening on :${PORT}`))

Three important things about the server:

  1. HMAC signature verification: without it, anyone who knows the public URL can inject fake tickets and burn through your LLM tokens. Always mandatory.
  2. Immediate 202 response + background processing: webhooks have timeouts (TeamSystem typically 10-15 seconds). An LLM call can take 5-20. Respond immediately that you’ve taken charge, process afterwards.
  3. timingSafeEqual for signature comparison: prevents timing attacks on credentials. Trivial to get right, trivial to get wrong.

Common traps

1. Polling instead of webhooks if you don’t have them. Older on-premise TeamSystem editions don’t have webhooks. The temptation is to poll every 60 seconds on all open tickets. DON’T: it burns rate limits and time. Sensible polling: query with filter dataApertura > last_check, every 2-5 minutes, with cursor tracking.

2. LLM output sent directly to the client. The generated draft must always go as internal note or draft, not as a response sent to the client. The human operator must confirm. Even if quality is high, the risk of hallucination on TeamSystem-specific names/dates/prices is real and must be mitigated with human review.

3. Prompt that doesn’t specify who the model is NOT. Always add in the system prompt: “You are not a lawyer, you are not a tax advisor, do not give specific compliance advice”. Italian clients ask legal/tax questions to anyone who answers, even on IT support tickets. Put an explicit guardrail in place.

4. Unmonitored LLM costs. Without token budget alerts, a misconfigured integration can consume 200-500 euros a day in minutes if it loops. Set budget alerts via the OpenAI/Anthropic console, and log tokens consumed per request.

5. Client data in clear in LLM logs. LLM providers log requests (with various retention policies). If you pass personal or sensitive data, enable the zero-retention agreement (Anthropic and OpenAI offer it for enterprise clients). For GDPR: exposing personal data to the provider is an extra-EU transfer for OpenAI/Anthropic and must be evaluated by your DPO.

Variations of the approach

Variant A: synthesis of historical client notes. Same pattern, but on “client opened”/“client viewed” event: the bridge reads all the client’s historical notes and produces a 5-8 line summary. Useful for operators who inherit clients managed by colleagues.

Variant B: automatic priority classification. The agent reads the newly opened ticket and proposes a priority based on content (keywords, perceived urgency, historical client value). Set only if confidence is high, otherwise leave it to the operator.

Variant C: follow-up email draft generation. On tickets closed N days ago, the agent generates a personalized follow-up draft. The operator decides whether to send. Typically improves customer retention by 5-15% in B2B contexts.

Variant D: information extraction from attached PDFs. Tickets often have attached PDFs (orders, invoices, communications). The agent parses them via the LLM’s vision API and extracts the relevant fields to automatically populate TeamSystem’s structured fields.

Limitations of this approach

1. Dependency on TeamSystem APIs. If the edition you use doesn’t expose the API you need (e.g. writing to custom tables), the workaround via direct DB access is possible but fragile (breaks at every TeamSystem update).

2. Perceived latency. Between webhook reception and note written in TeamSystem typically 8-20 seconds pass (API call + LLM call + write). For many use cases it’s acceptable. For synchronous use cases (user waiting) it’s too much: you need a different approach (streaming response directly in UI).

3. Costs under load. Above 500 LLM calls per day, costs grow significantly. At 5,000/day the gpt-4o-mini model costs about 5-10 euros per day in tokens alone. More powerful models (gpt-4o, Claude Sonnet) are 3-5x. Plan the budget realistically.

4. Doesn’t replace domain knowledge. The agent can respond well on standard FAQs. On questions that require knowledge of the specific contract of the client, of the operational history, of known exceptions, the LLM alone can’t manage. You need a RAG layer (vector store with company knowledge base) which is a separate project.

FAQ

How much does this integration cost on a monthly basis?

For a typical scenario (Italian SME, 500-1,500 tickets per month passed through the agent), the LLM cost settles around 30-120 euros per month with gpt-4o-mini, 100-400 euros per month with Claude Sonnet or gpt-4o. Add 30-80 euros per month for bridge hosting (Fly.io or Vercel) and basic monitoring.

Does TeamSystem allow writing to all fields via API?

It depends on the edition. Enterprise and Cloud editions expose REST APIs for most standard objects (clients, tickets, activities, notes, attachments). Custom company fields and vertical modules (construction management, fashion, etc.) often require access to specific endpoints or additional authorizations. Check with your TeamSystem contact before starting.

Can self-hosted open-source LLMs be used instead of GPT/Claude?

Yes, and for highly sensitive data it may be mandatory for compliance. Models like Llama 3.3 70B or Mistral Large via Ollama on NVIDIA H100/L40S GPUs provide acceptable quality for many use cases. Initial cost 5-15k euros for the GPU or 1-3 euros/hour for cloud GPU instances. Worth it if volumes are high and cloud token costs exceed 500 euros per month, or if there are regulatory constraints on data residency.

How are sensitive personal data handled (e.g. clients in healthcare)?

Three levels to consider: (1) signing a zero-retention agreement with OpenAI/Anthropic to avoid retention in provider logs; (2) DPA (Data Processing Agreement) under GDPR governing roles and extra-EU transfer; (3) for strictly sensitive data (healthcare under GDPR art. 9), evaluate self-hosted LLMs on Italian/European infrastructure to avoid extra-EU transfer. The choice depends on your DPO.

How do you test that the prompt actually works?

A manual test set of 20-50 real (anonymized) tickets, with a reference human response. The prompt is iterated until quality is acceptable on at least 85% of cases. Eval tools like Promptfoo or LangSmith automate this phase when volume grows.

Conclusion

The pattern for integrating GPT with TeamSystem (and with most Italian business management systems that expose decent REST APIs) is repeatable: incoming webhook, retrieval of context data, LLM call with native Italian prompt, writing as note or draft, human review. The real value comes when you iterate on the prompt on your own real tickets, not on the prompt copied from a blog post (including this one).

If you’re evaluating an AI integration on your TeamSystem or equivalent business management system, let’s talk. We can start from a 2-3 week POC on a single flow, to validate the technical fit before committing to a full project.

To go deeper: the pillar page AI agents, the page dedicated to AI integration in the business management system, and the customer service AI agent page where this pattern is used in client support scenarios.

Tags: agenti-aiteamsystemllmintegrazionetutorialtypescript