@trueyy-sdk/node
Example (Express)
A complete Express backend that exercises every @trueyy-sdk/node API.
A reference ATS backend that touches every part of @trueyy-sdk/node — interviews, tokens, reports, team, billing, and webhooks. In a real app these routes would sit behind your auth; here they're plain routes so you can see the SDK call each one maps to.
Runnable source
The full example lives in the SDK repo at examples/express-ats. Clone it, set TRUEYY_API_KEY, and run it.
Setup
import express from "express";
import { Trueyy, type SessionRole } from "@trueyy-sdk/node";
const trueyy = new Trueyy({
apiKey: process.env.TRUEYY_API_KEY!, // tk_live_ or tk_test_
});
const app = express();
const PORT = Number(process.env.PORT ?? 3000);
// Async route wrapper — forwards the SDK's HTTP status (e.g. 404, 422) through.
const h =
(fn: (req: express.Request, res: express.Response) => Promise<void>) =>
(req: express.Request, res: express.Response) =>
fn(req, res).catch((err: unknown) => {
const status = (err as { status?: number }).status ?? 500;
res.status(status).json({ error: (err as Error).message });
});Schedule an interview
The core flow: invite the interviewer, create the interview + round 1, then mint browser tokens.
app.post(
"/internal/schedule-interview",
express.json(),
h(async (req, res) => {
// 1. Ensure the interviewer exists, then reference them by id.
await trueyy.team.invite({ email: req.body.interviewer.email, role: "Member" });
const { members } = await trueyy.team.members();
const interviewer = members.find((m) => m.email === req.body.interviewer.email);
if (!interviewer) {
res.status(409).json({ error: "Interviewer must accept their invite first" });
return;
}
// 2. Create the interview + round 1.
const interview = await trueyy.interviews.create({
role: req.body.role ?? "Interview",
candidate_email: req.body.candidate.email,
candidate_first_name: req.body.candidate.first_name,
candidate_last_name: req.body.candidate.last_name,
first_round: {
round_name: req.body.round_name ?? "Round 1",
interviewer_user_id: interviewer.id,
scheduled_start_at: req.body.start,
scheduled_end_at: req.body.end,
timezone: req.body.timezone ?? "UTC",
meeting_link: req.body.zoom_url,
},
});
// 3. Mint browser tokens for your candidate + interviewer UIs.
const candidate = await trueyy.tokens.mint(interview.round_id, "candidate");
const interviewerToken = await trueyy.tokens.mint(interview.round_id, "interviewer");
res.json({
interview_id: interview.id,
round_id: interview.round_id,
candidate,
interviewer: interviewerToken,
});
}),
);Interviews, tokens, reports
// List interviews (paginated; optional ?search=&limit=&offset=).
app.get("/internal/interviews", h(async (req, res) => {
const page = await trueyy.interviews.list({
limit: req.query.limit ? Number(req.query.limit) : undefined,
offset: req.query.offset ? Number(req.query.offset) : undefined,
search: typeof req.query.search === "string" ? req.query.search : undefined,
});
res.json(page);
}));
// One interview with all its rounds + per-round analysis.
app.get("/internal/interviews/:id", h(async (req, res) => {
res.json(await trueyy.interviews.get(req.params.id!));
}));
// Add another round to an existing interview.
app.post("/internal/interviews/:id/rounds", express.json(), h(async (req, res) => {
const round = await trueyy.interviews.addRound(req.params.id!, {
round_name: req.body.round_name,
interviewer_user_id: req.body.interviewer_user_id,
scheduled_start_at: req.body.start,
scheduled_end_at: req.body.end,
timezone: req.body.timezone ?? "UTC",
meeting_link: req.body.zoom_url,
});
res.json(round);
}));
// Cancel the interview (and every round that hasn't started).
app.delete("/internal/interviews/:id", h(async (req, res) => {
await trueyy.interviews.cancel(req.params.id!);
res.sendStatus(204);
}));
// Mint (or refresh) a short-lived browser token for a round.
app.post("/internal/rounds/:roundId/token", express.json(), h(async (req, res) => {
const role = (req.body.role ?? "candidate") as SessionRole;
res.json(await trueyy.tokens.mint(req.params.roundId!, role));
}));
// The final per-round report.
app.get("/internal/reports/:roundId", h(async (req, res) => {
res.json(await trueyy.reports.get(req.params.roundId!));
}));Team & billing
app.post("/internal/team/invite", express.json(), h(async (req, res) => {
res.json(await trueyy.team.invite({ email: req.body.email, role: req.body.role ?? "Member" }));
}));
app.get("/internal/team/members", h(async (_req, res) => {
res.json(await trueyy.team.members());
}));
// Billing is read-only — plan changes happen in the dashboard, never via the SDK.
app.get("/internal/billing", h(async (_req, res) => {
const [plans, subscription, usage, invoices] = await Promise.all([
trueyy.billing.plans(),
trueyy.billing.subscription(),
trueyy.billing.usage(),
trueyy.billing.invoices(),
]);
res.json({ plans, subscription, usage, invoices });
}));Webhooks
Mount with express.raw() so HMAC verification runs against the raw bytes.
app.post(
"/webhooks/trueyy",
express.raw({ type: "application/json" }),
trueyy.webhooks.verify(process.env.TRUEYY_WEBHOOK_SECRET!),
(req, res) => {
const { event_type, data } = req.trueyy!.event;
switch (event_type) {
case "session.ready": console.log("Candidate joined:", data); break;
case "session.transcript_segment": console.log("Transcript fragment:", data); break;
case "session.risk_pulse": console.log("Risk pulse:", data); break;
case "session.window_result": console.log("Window analysis:", data); break;
case "session.image_analysis_result": console.log("Image analysis:", data); break;
case "session.ended": console.log("Session ended:", data); break;
case "session.report_ready": console.log("Report ready:", data); break; // → reports.get(round_id)
case "session.cancelled": console.log("Session cancelled:", data); break;
default: console.log("Unhandled:", event_type, data);
}
res.sendStatus(200);
},
);
app.listen(PORT, () => console.log(`ATS example on http://localhost:${PORT}`));See the Webhooks reference for the full event catalog and delivery guarantees.