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.

  1. 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.
  2. Create an API key: Settings → Developers, or Integrations → REST API. Scope it to `intelligence`, `helpdesk`, or both.
  3. Copy the agent public ID from Agent → Configuration. Every call needs it as `agentId`.
  4. Store `HUMANER_API_KEY` and `HUMANER_AGENT_ID` as server-side secrets. Never expose `hm_live_` keys to a browser.
  5. Add a retrieval step before any factual answer. Gate on the `grounded` flag: if false, do not improvise — escalate.
  6. Add `match-runbook` for recurring procedures and follow the returned steps.
  7. Add `create-ticket` on explicit human requests and on ungrounded retrieval, sending the transcript.
  8. 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.
  9. Optional: `remember` / `memory` with a stable `visitorId` from the operator's own auth layer.
  10. 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.

ConcernOwner
Chat UI, widget, transportOperator's agent
Model choice, prompts, tone, personaOperator's agent
Conversation state and turn loopOperator's agent
Visitor identity (`visitorId`)Operator's agent
Knowledge base and retrievalHumaner
Runbook matchingHumaner
Guardrail topic list (advisory)Humaner
Tickets, routing, urgency, SLAHumaner
Visitor memory storeHumaner
Request log and usageHumaner

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.io

Server-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 advice

Tools

ToolREST pathBillable
search_knowledgePOST /api/v1/intelligence/retrieveYes
match_runbookPOST /api/v1/intelligence/match-runbookYes
create_ticketPOST /api/v1/intelligence/create-ticketYes
remember_factPOST /api/v1/intelligence/rememberYes
get_visitor_memoryPOST /api/v1/intelligence/memoryYes
identify_visitorPOST /api/v1/intelligence/identifyNo
check_escalation_signalPOST /api/v1/intelligence/escalationNo

Verify

  1. Call retrieve with a question your knowledge base covers. Expect `grounded: true`.
  2. Call retrieve with nonsense. Expect `grounded: false` and confirm your code escalates instead of answering.
  3. Force a ticket and confirm it lands in Desk with the transcript attached.
  4. Open Agent → Analytics. Confirm the calls are listed with status 200 and sane latency.
  5. 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

StatusMeaningFix
400Invalid JSON, missing agentId, or tool rejected inputMatch the request body to the tool schema
401Missing, invalid, or wrong-organization keyRe-issue the key; confirm the agent belongs to that organization
403Key missing scope, or plan lacks API accessAdd the scope, or confirm the plan is Custom
404Agent not found or pausedCheck the public ID and unpause the agent

Next