Build a connector. Give your agent real data.
A SupportCore connector is a small HTTPS service you host. It lets your support agent look up live data and run the actions you allow — order status, returns, warranty, account changes — all declared by a manifest so SupportCore discovers your capabilities automatically. One contract, any stack, no custom work on our side.
01Quick start
Host the endpoint
Serve a small service at /supportcore/v1 over HTTPS, guarded by a bearer key.
Publish a manifest
Declare your lookups and actions at GET /manifest. Each becomes an agent tool.
Connect it
Paste the base URL + key into Console → Connections. Your agent starts using it.
02Authentication
SupportCore sends a bearer key you issue in the Connections tab. Verify it on every request; reject anything else.
POST https://api.yourapp.com/supportcore/v1/lookup/customer Authorization: Bearer sc_live_9f2c…a71b Content-Type: application/json Idempotency-Key: e0b6…c455 # on actions; safe to retry
HTTPS only. Compare the key in constant time; return 401 on mismatch. Rotate it from
the Connections tab any time — no redeploy. Optionally verify an X-SupportCore-Signature HMAC of the body.
03The manifest — where you define capabilities
The heart of the spec. SupportCore fetches your manifest and turns every entry into a tool the agent can use. To teach the agent a new action — say, a note on an order — add one object here and implement its endpoint. That's it.
{
"connector": "YourApp", "version": "1.0",
"lookups": [
{ "id": "customer", "title": "Customer profile",
"description": "Plan, status and recent activity for an email",
"params": [ { "name": "email", "type": "string", "required": true } ],
"returns": "plan, status, signup_date, last_active" }
],
"actions": [
{ "id": "add_order_note", "title": "Add a note to an order",
"params": [ { "name": "order_id", "type": "string", "required": true, "pattern": "^[0-9]+$" },
{ "name": "note", "type": "string", "required": true } ],
"approval": "optional", "reversible": true },
{ "id": "cancel_subscription", "title": "Cancel a subscription",
"params": [ { "name": "email", "type": "string", "required": true } ],
"approval": "required", "reversible": false }
]
}
| Field | Meaning |
|---|---|
id | Stable name. SupportCore calls /lookup/{id} or /action/{id}. Never rename a live id. |
params[] | Each: name, type, required, optional pattern (regex). SupportCore validates against pattern before calling you. |
approval | required gates the action behind a human click. Default required when omitted. |
reversible | Irreversible actions always get an extra confirm and never auto-run. |
04Lookups · read
Return support-relevant, non-sensitive data. On no match, return 200 with { "found": false } — not a 404.
{ "email": "kees@example.com" }{ "found": true,
"data": { "plan": "Pro", "status": "active",
"open_invoices": 0 } }05Actions · write
The logic and permission checks live in your backend — SupportCore only requests it. Return a short human
message; the agent may quote it. Honour Idempotency-Key so the same key never acts twice.
{ "email": "kees@example.com",
"_meta": { "approved_by": "stephen@urenapp.eu",
"ticket": "89155065-…" } }{ "ok": true,
"message": "Cancelled, effective 1 Aug.",
"reference": "CANCEL-7731" }approval:"required" and
reversible:false. SupportCore never auto-runs these — a human confirms first. On a business-rule
failure, return 200 with { "ok": false, "message": "…" } so the agent can explain it.06Reference data & follow-up questions
The agent is only as good as what it can see. Two kinds of lookups make it genuinely helpful:
Record lookups — “this specific customer / order”
Keyed by an identifier (email, order id). We strongly recommend backing these with a
read-only database account — grant SELECT on just the tables or views support needs
(customers, orders, bookings, invoices) so the agent can see history and status without any write
access or exposure of sensitive fields.
Catalogue lookups — “what do we even offer?”
Expose your options too, not just records. A hotel publishes a room_types lookup
(types, capacity, price); a shop publishes shipping_options. This is what lets the agent
reason and ask the right follow-up instead of guessing.
room_types, sees that pricing depends on dates and room type,
notices the guest gave neither, and replies asking for them — then, once answered, prices it exactly.
You didn't script that conversation; you just published the catalogue and the agent did the reasoning.Structured catalogue — categories & articles, filterable
Take it one step further. Mark a catalogue lookup "catalog": true and point SupportCore at
the list of items with an items descriptor. SupportCore imports it into a structured,
category-driven Catalog — every entry becomes an article, grouped into categories —
and refreshes it on every sync, so a tariff or stock change in your backoffice flows straight through.
This is what lets the agent answer “which pitch fits a 5 m caravan with electricity under €40?”
by filtering real data instead of guessing from prose.
Name the field that holds each entry's category with items.category and SupportCore
groups the articles under it. A category is a first-class thing: the company adds shared remarks and
specifications to it once (e.g. “10 A electricity, dogs allowed”), and every article inside
inherits them — so the same fact is never repeated per row, and the agent reads it as one truth per
category. Omit category for a flat catalogue. The exact same shape works for a booking system,
a webshop, a bakery or a butcher — one universal contract, no domain assumptions.
// manifest entry — a catalogue lookup that becomes a category-driven Catalog { "id": "products", "title": "Product catalogue", "catalog": true, "personal": false, "items": { "list": "products", "key": "sku", "title": "name", "category": "category" }, "params": [] } // the lookup's response — each entry is one article; `category` groups them { "products": [ { "sku": "CB-COL-250", "name": "Colombia Supremo 250g", "category": "Koffiebonen", "price": 8.5, "in_stock": true, "weight_g": 250 }, { "sku": "TH-EARL-50", "name": "Earl Grey losse thee 50g", "category": "Thee", "price": 5.25, "in_stock": true, "weight_g": 50 } ] }
| Field | Meaning |
|---|---|
catalog | Marks this lookup as the company's catalogue — SupportCore imports it into the structured Catalog, not just a knowledge article. |
items.list | The key in your response body that holds the array of items. |
items.key | Each item's stable identity (e.g. sku, code) — so the next sync refreshes the same article instead of duplicating it. |
items.title | The field that names each article for a human (the row header). |
items.category | Optional. The field that names each article's category. Present → SupportCore groups the articles into categories (with shared, inherited remarks & specs). Absent → a flat list. |
Common fields map onto universal columns automatically — price/price_per_night/tarief → price,
description → description, image → photo, stock/in_stock/availability → availability. Everything
else (like weight_g) becomes its own typed column — text, number or yes/no, inferred from the values. Return the full list;
SupportCore keeps it in step on each sync. A separate per-item lookup that answers the live price or availability keeps those
columns live rather than a stored snapshot.
Rule of thumb: if a human agent would need to look something up or ask a clarifying question to answer, publish a lookup for it. The agent will use it the same way.
07Errors & limits
| Rule | Detail |
|---|---|
| Latency | Respond within 5 seconds — SupportCore times out after that. |
| Errors | HTTP status for transport/auth/server; { ok:false } for business outcomes. Shape: { "error": { "code", "message" } }. |
| Versioning | Pin the major version in the path (/v1/). Add fields freely; never remove one within a version. |
| Rate limit | Return 429 when needed — SupportCore backs off and retries. |
08Security
- ✓Least privilege. Read-only for lookups; writes only through the specific operations your actions implement. Never an admin account.
- ✓Expose only what support needs. No password hashes, full card numbers or unrelated personal data — even behind a valid key.
- ✓Validate server-side. The manifest
patternis a first gate; re-check ownership and entitlements in your handler. - ✓Rotate & log. Rotate the key; log every call with its
ticketandapproved_by.
09Full minimal example
const app = require("express")(); app.use(require("express").json()); const KEY = process.env.SUPPORTCORE_KEY; app.use((req, res, next) => { // 1 · auth const t = (req.headers.authorization || "").replace("Bearer ", ""); if (t !== KEY) return res.status(401).json({ error: { code: "unauthorized" } }); next(); }); app.get("/supportcore/v1/manifest", (_, res) => res.json({ // 2 · manifest connector: "YourApp", version: "1.0", lookups: [{ id: "customer", title: "Customer profile", params: [{ name: "email", type: "string", required: true }] }], actions: [{ id: "add_order_note", title: "Add order note", approval: "optional", params: [{ name: "order_id", type: "string", required: true, pattern: "^[0-9]+$" }, { name: "note", type: "string", required: true }] }] })); app.post("/supportcore/v1/lookup/customer", async (req, res) => { // 3 · lookup const u = await db.user(req.body.email); res.json(u ? { found: true, data: { plan: u.plan, status: u.status } } : { found: false }); }); app.post("/supportcore/v1/action/add_order_note", async (req, res) => { // 4 · action await db.addNote(req.body.order_id, req.body.note); res.json({ ok: true, message: "Note added to order " + req.body.order_id }); }); app.listen(8080);
10Readiness checklist
- ✓HTTPS at a stable
/supportcore/v1base URL. - ✓
GET /manifestlists every lookup and action; eachidhas a handler. - ✓Bearer auth enforced;
401on mismatch; key rotatable. - ✓Lookups return
{ found, data }; actions{ ok, message }; under 5s. - ✓Idempotency on actions; destructive ones
approval:"required". - ✓Read-only, least-privilege data; no sensitive fields exposed.
11The other direction — drive tickets from your systems
The Connector API above is how SupportCore calls into your systems. The Support API is the
reverse: how your systems talk to SupportCore. Open a ticket from a chat widget, an app or a helpdesk;
read the AI's suggested reply; post an answer; close the conversation. Every ticket runs through the same
pipeline, trust model and learning loop as email — so a chat widget is just channel:"chat".
Building support into your own app or portal? Use channel:"api": that channel never sends
email — storing the reply and firing reply.sent is the delivery, and your app shows it.
Per category you choose in Console → Settings → AI agent (column App/API) whether the agent
answers instantly, chat-style, or every reply waits for human approval first.
POST https://app.supportcore.ai/api/v1/tickets Authorization: Bearer sc_live_… # create one in Console → Developers Content-Type: application/json
Keys are per-company, hashed at rest and revocable. Each key resolves to exactly one company; all data stays isolated by the same row-level security as the rest of SupportCore.
12Ticket endpoints
| Endpoint | Does |
|---|---|
POST /api/v1/tickets | Open a ticket and run the AI. Returns the ticket + suggested reply. |
POST /api/v1/tickets/{id}/messages | Add a customer follow-up; re-runs the pipeline on the full thread. |
GET /api/v1/tickets/{id} | Full state: every message, the current draft, its confidence. |
GET /api/v1/tickets | List tickets, newest first. ?status=, ?limit=. |
POST /api/v1/tickets/{id}/reply | Record an outbound reply and fire reply.sent. |
POST /api/v1/tickets/{id}/close | Close it (fires ticket.closed). /reopen to undo (fires ticket.reopened). |
{ "customer_email": "kees@acme.com",
"channel": "api",
"message": "How do I reset my password?",
"external_id": "app-ticket-8841",
"verified": true }{ "id": "a1f2…", "status": "open",
"draft": {
"status": "pending", "confidence": 0.94,
"needs_human": false,
"suggested_reply": "Go to Settings → …" } }external_id is optional and makes creation idempotent — a retried call returns the same
ticket instead of opening a duplicate. When draft.needs_human is true the AI handed off;
write the reply yourself and POST it to /reply.
verified: true tells SupportCore that your system already authenticated
this customer (a logged-in app user, a portal session). The conversation then starts fully verified: the agent
skips identity questions and connector lookups on the customer's own record are allowed immediately. Only set it
when the email address is genuinely proven — the claim is made under your API key and lands in the audit log.
Delivery on channel:"api": when the category's App/API column is on auto,
the reply is delivered in the API response and reply.sent fires immediately (same guards as chat:
permission matrix for actions, no unbacked "it's been sent" claims, paywall). On control, the draft waits
in the inbox; human approval then delivers it along the same webhook — never by email.
13Webhooks — get notified the moment anything happens
Register an endpoint in Console → Developers and SupportCore POSTs a signed JSON event to it. Pick the events you care about, or all of them. Deliveries retry with backoff and are logged in plain language so anyone can see what happened — no engineer required.
| Event | Fires when |
|---|---|
ticket.created | A new ticket arrives — email, chat or API. |
draft.ready | The AI produced a reply, ready to approve or auto-send. |
ticket.escalated | The AI handed off — a human must write this one. |
reply.sent | A reply reached the customer — email, chat or API. data carries
channel and the reply message, so an app can render it without an extra GET. |
ticket.closed | A ticket was closed. |
ticket.reopened | A closed ticket was reopened via the Support API. |
action.proposed / .executed / .failed | A connector action was proposed, ran, or failed. |
{ "event": "ticket.created",
"tenant": "acme",
"at": 1753142400,
"data": { "ticket_id": "a1f2…",
"customer_email": "guest@acme.com",
"channel": "chat" } }X-SupportCore-Event: ticket.created X-SupportCore-Timestamp: 1753142400 X-SupportCore-Signature: sha256=9f2c…
Answer with any 2xx to acknowledge. Anything else (or a timeout) is retried up to three
times with backoff, then marked failed in the delivery log.
14Verify the signature
Every delivery is signed so you can trust it came from SupportCore and was not tampered with or replayed.
Recompute the HMAC over {timestamp}.{raw_body} with your endpoint secret and compare.
const crypto = require("crypto"); function verify(req, secret) { const ts = req.headers["x-supportcore-timestamp"]; const sig = req.headers["x-supportcore-signature"]; const mine = "sha256=" + crypto .createHmac("sha256", secret) .update(ts + "." + req.rawBody) // the exact bytes we sent .digest("hex"); return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(mine)); }