Custom — Agent Brief
Copy this Markdown into an AI coding agent. It covers wiring an existing agent into Humaner Intelligence and Helpdesk over REST: API key and scopes, retrieval gated on the grounded flag, runbooks, guardrails, tickets, memory, and how to verify the wiring in the Analytics request log.
Instructions for the integrating agent
Your job
Wire the operator's existing agent into Humaner Intelligence over REST from their backend. Do not build a chat UI, do not adopt a Humaner persona, and do not call hosted chat or widget endpoints — they return 403 on Custom. Finish by confirming calls appear in Agent → Analytics.
- Confirm the plan is Custom on Humaner Cloud. If they want the hosted widget and persona, stop and use the Cloud agent brief. If they want to run everything themselves, use the Self-hosting brief.
- Create an API key: Settings → Developers, or Integrations → REST API. Scope it to `intelligence`, `helpdesk`, or both.
- Copy the agent public ID from Agent → Configuration. Every call needs it as `agentId`.
- Store `HUMANER_API_KEY` and `HUMANER_AGENT_ID` as server-side secrets. Never expose `hm_live_` keys to a browser.
- Add a retrieval step before any factual answer. Gate on the `grounded` flag: if false, do not improvise — escalate.
- Add `match-runbook` for recurring procedures and follow the returned steps.
- Add `create-ticket` on explicit human requests and on ungrounded retrieval, sending the transcript.
- Inject the `forbiddenTopics` array returned on every `retrieve` response into the system prompt as a refusal instruction. It is managed in Agent → Configuration; Humaner cannot enforce it because you own the prompt.
- Optional: `remember` / `memory` with a stable `visitorId` from the operator's own auth layer.
- Verify in Agent → Analytics that calls appear with status 200.
Quickstart: https://docs.humaner.io/custom/quickstart · Tools reference: https://docs.humaner.io/custom/reference · Cloud brief: https://docs.humaner.io/agent-brief · Self-hosting brief: https://docs.humaner.io/oss/agent-brief
Responsibility split
Custom means the operator keeps their agent and Humaner becomes the support backend. Nothing about how the agent speaks is configured in Humaner.
| Concern | Owner |
|---|---|
| Chat UI, widget, transport | Operator's agent |
| Model choice, prompts, tone, persona | Operator's agent |
| Conversation state and turn loop | Operator's agent |
| Visitor identity (`visitorId`) | Operator's agent |
| Knowledge base and retrieval | Humaner |
| Runbook matching | Humaner |
| Guardrail topic list (advisory) | Humaner |
| Tickets, routing, urgency, SLA | Humaner |
| Visitor memory store | Humaner |
| Request log and usage | Humaner |
No hosted persona on Custom
The dashboard has no Persona editor on Custom. The agent workspace is Configuration, Knowledge, Runbooks, Escalation, Analytics — guardrails sit inside Configuration, and the request log inside Analytics. History is absent because it only records hosted chat.
Environment
.env
HUMANER_API_KEY=hm_live_xxxxxxxx
HUMANER_AGENT_ID=YOUR_AGENT_PUBLIC_ID
HUMANER_BASE_URL=https://app.humaner.ioServer-side only
Intelligence endpoints reject browser Origin auth. If the key reaches client JavaScript, rotate it.
Minimal client
humaner.ts
const BASE = process.env.HUMANER_BASE_URL!;
const KEY = process.env.HUMANER_API_KEY!;
const AGENT_ID = process.env.HUMANER_AGENT_ID!;
async function call<T>(path: string, body: Record<string, unknown>): Promise<T> {
const res = await fetch(`${BASE}/api/v1/intelligence/${path}`, {
method: "POST",
headers: {
Authorization: `Bearer ${KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({ agentId: AGENT_ID, ...body })
});
if (!res.ok) {
throw new Error(`Humaner ${path} failed: ${res.status}`);
}
return res.json() as Promise<T>;
}
export const retrieve = (query: string) =>
call<{
chunks: {
content: string;
heading: string | null;
source: string;
score: number;
}[];
grounded: boolean;
forbiddenTopics: string[];
}>("retrieve", { query });
export const matchRunbook = (ticketContext: string) =>
call<{
matched: boolean;
runbook: { id: string; title: string | null } | null;
}>("match-runbook", { ticketContext });
export const createTicket = (input: {
subject: string;
visitorEmail: string; // required — collect it before escalating
transcript: { role: string; content: string }[];
}) =>
call<{ ticketId: string; ticketNumber: number; routedTo: string }>(
"create-ticket",
input
);Reply loop
Retrieve first, gate on grounded, escalate when it is false. This single rule is what separates a reliable Custom integration from one that hallucinates.
import { createTicket, matchRunbook, retrieve } from "./humaner";
export async function answer(
message: string,
visitorEmail: string, // tickets require it — collect it before escalating
transcript: { role: string; content: string }[]
) {
// Independent calls: fire both at once.
const [match, { chunks, grounded, forbiddenTopics }] = await Promise.all([
matchRunbook(message),
retrieve(message)
]);
if (!grounded) {
const ticket = await createTicket({
subject: message.slice(0, 120),
visitorEmail,
transcript
});
return `I have passed this to a teammate. Ticket #${ticket.ticketNumber}.`;
}
// Your model, your prompt, your voice. The matched runbook names the
// procedure you wrote in the dashboard; steer the reply toward it.
return generateReply({
message,
context: chunks,
forbiddenTopics,
runbookTitle: match.runbook?.title ?? null,
transcript
});
}Parallelize where you can
Runbook matching and retrieval are independent, which is why the loop above fires both with Promise.all instead of awaiting them in sequence.
Guardrails in your prompt
Every retrieve response carries `forbiddenTopics` — the list managed in Agent → Configuration. Inject it into your system prompt on every turn; when the master switch there is off the array comes back empty. Enforcement is yours.
Never discuss the following topics. Decline politely and offer a ticket:
- Competitor feature comparisons
- Confirming unannounced features or roadmap dates
- Legal or compliance adviceTools
| Tool | REST path | Billable |
|---|---|---|
| search_knowledge | POST /api/v1/intelligence/retrieve | Yes |
| match_runbook | POST /api/v1/intelligence/match-runbook | Yes |
| create_ticket | POST /api/v1/intelligence/create-ticket | Yes |
| remember_fact | POST /api/v1/intelligence/remember | Yes |
| get_visitor_memory | POST /api/v1/intelligence/memory | Yes |
| identify_visitor | POST /api/v1/intelligence/identify | No |
| check_escalation_signal | POST /api/v1/intelligence/escalation | No |
Verify
- Call retrieve with a question your knowledge base covers. Expect `grounded: true`.
- Call retrieve with nonsense. Expect `grounded: false` and confirm your code escalates instead of answering.
- Force a ticket and confirm it lands in Desk with the transcript attached.
- Open Agent → Analytics. Confirm the calls are listed with status 200 and sane latency.
- Confirm hosted endpoints are closed: POST /api/v1/chat should return 403.
Empty log?
Only authorized calls are recorded. A bad key or unknown agentId is rejected before it can be attributed to an agent, so nothing is written. Check both first.
Errors
| Status | Meaning | Fix |
|---|---|---|
| 400 | Invalid JSON, missing agentId, or tool rejected input | Match the request body to the tool schema |
| 401 | Missing, invalid, or wrong-organization key | Re-issue the key; confirm the agent belongs to that organization |
| 403 | Key missing scope, or plan lacks API access | Add the scope, or confirm the plan is Custom |
| 404 | Agent not found or paused | Check the public ID and unpause the agent |