@mnemix-ai/bland-kit
v0.3.0
Published
Mnemix integration kit for Bland.ai voice pathways — pre-call enrichment + post-call memory write-back.
Maintainers
Readme
@mnemix-ai/bland-kit
Mnemix integration kit for Bland.ai voice pathways. Pre-call enrichment + post-call memory write-back.
Why this exists
Bland builds voice pathways. This kit maps an appropriate Bland webhook step to Mnemix's frozen caller-memory API and can return stored caller memory with nullable Trestle and Twilio Lookup enrichment for your pathway to use.
After a verified post-call webhook, the kit submits completed-call context to POST /v1/calls/end. A successful acknowledgement is not persistence proof; verify the update through a later read before depending on it. The kit is intentionally thin: it verifies Bland webhooks when configured, calls the frozen public Mnemix endpoints needed by this integration, builds pathway variables, and degrades to safe defaults when external data is unavailable. This is a custom integration kit, not a native or certified Bland adapter.
Install
npm install @mnemix-ai/bland-kit @mnemix-ai/clientQuickstart: Cloudflare Worker
import { Mnemix } from "@mnemix-ai/client";
import {
handleBlandPostCall,
handleBlandPreCall,
type BlandPostCallPayload,
type BlandPreCallPayload,
} from "@mnemix-ai/bland-kit";
interface Env {
MNEMIX_KEY: string;
MNEMIX_BASE_URL?: string;
BLAND_WEBHOOK_SECRET?: string;
}
export default {
async fetch(req: Request, env: Env): Promise<Response> {
if (req.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const url = new URL(req.url);
const rawBody = await req.text();
const signature = req.headers.get("x-bland-signature") ?? undefined;
const mnemix = new Mnemix({
apiKey: env.MNEMIX_KEY,
baseUrl: env.MNEMIX_BASE_URL || undefined,
});
const opts = {
mnemix,
blandWebhookSecret: env.BLAND_WEBHOOK_SECRET,
};
if (url.pathname === "/bland/pre-call") {
const payload = JSON.parse(rawBody) as BlandPreCallPayload;
const ctx = await handleBlandPreCall(opts, payload, {
signature,
rawBody,
tone: "warm",
agentName: "the assistant",
});
return Response.json({ variables: ctx.variables, system_prompt: ctx.system_prompt });
}
if (url.pathname === "/bland/post-call") {
const payload = JSON.parse(rawBody) as BlandPostCallPayload;
return Response.json(await handleBlandPostCall(opts, payload));
}
return new Response("Not found", { status: 404 });
},
};Quickstart: Node Express
import express from "express";
import { Mnemix } from "@mnemix-ai/client";
import { handleBlandPostCall, handleBlandPreCall } from "@mnemix-ai/bland-kit";
const app = express();
app.use(express.text({ type: "application/json" }));
const mnemix = new Mnemix({
apiKey: process.env.MNEMIX_KEY!,
baseUrl: process.env.MNEMIX_BASE_URL || undefined,
});
app.post("/bland/pre-call", async (req, res) => {
const rawBody = req.body as string;
const ctx = await handleBlandPreCall(
{
mnemix,
blandWebhookSecret: process.env.BLAND_WEBHOOK_SECRET,
},
JSON.parse(rawBody),
{
signature: req.header("x-bland-signature") ?? undefined,
rawBody,
tone: "warm",
},
);
res.json({ variables: ctx.variables, system_prompt: ctx.system_prompt });
});
app.post("/bland/post-call", async (req, res) => {
const result = await handleBlandPostCall({ mnemix }, JSON.parse(req.body as string));
res.json(result);
});
app.listen(process.env.PORT ?? 8787);Quickstart: Next.js App Router
// app/api/bland/[hook]/route.ts
import { Mnemix } from "@mnemix-ai/client";
import { handleBlandPostCall, handleBlandPreCall } from "@mnemix-ai/bland-kit";
const mnemix = new Mnemix({
apiKey: process.env.MNEMIX_KEY!,
baseUrl: process.env.MNEMIX_BASE_URL || undefined,
});
export async function POST(
req: Request,
{ params }: { params: { hook: string } },
) {
const rawBody = await req.text();
const opts = {
mnemix,
blandWebhookSecret: process.env.BLAND_WEBHOOK_SECRET,
};
if (params.hook === "pre-call") {
const ctx = await handleBlandPreCall(opts, JSON.parse(rawBody), {
signature: req.headers.get("x-bland-signature") ?? undefined,
rawBody,
tone: "professional",
});
return Response.json({ variables: ctx.variables, system_prompt: ctx.system_prompt });
}
if (params.hook === "post-call") {
return Response.json(await handleBlandPostCall(opts, JSON.parse(rawBody)));
}
return Response.json({ error: "unknown hook" }, { status: 404 });
}What You Get Back
handleBlandPreCall() returns a BlandPreCallContext:
interface BlandPreCallContext {
variables: {
caller_name: string;
is_returning: boolean;
last_intent: string | null;
last_call_summary: string | null;
carrier: string | null;
line_type: string | null;
company: string | null;
role: string | null;
industry: string | null;
suggested_intent: string | null;
};
system_prompt: string;
mnemix: {
trace_id: string;
known: boolean;
caller_id: string;
memory_age_ms: number;
timing_ms: { total: number; memory_ms: number; enrichment_ms: number };
};
}variables is the safe subset Bland should template into prompts and branch conditions. system_prompt is a ready-to-use prompt assembled from those variables. mnemix carries trace and timing data for logs, support, and debugging.
System Prompt Customization
import { buildSystemPrompt } from "@mnemix-ai/bland-kit";
const systemPrompt = buildSystemPrompt(ctx.variables, {
tone: "warm", // "warm" | "professional" | "casual"
agentName: "Avery",
brandName: "Northstar Support",
fallbackGreeting: "Hi there",
});The builder does not invent missing history. If Mnemix returns no memory, the prompt tells the agent to ask rather than assume.
Webhook Signature Verification
Set BLAND_WEBHOOK_SECRET in your deployment and pass blandWebhookSecret to the kit. For pre-call webhooks, also pass the raw request body and the X-Bland-Signature header to handleBlandPreCall().
The kit verifies HMAC-SHA256 signatures through verifyBlandSignature(). If signature verification fails, the pre-call path returns a safe default context instead of exposing enriched caller data.
Graceful Degradation
Voice pathways should not block on enrichment. If Mnemix times out, returns a 5xx, receives a malformed phone number, or returns partial enrichment, the kit returns defaults such as caller_name: "there" and is_returning: false. Bland can continue the call while Mnemix logs the failure path.
Partial enrichment is still useful. For example, Twilio Lookup can supply carrier data while Trestle is unavailable.
Errors
BlandKitError is the base typed error for kit-level failures.
BlandKitSignatureError represents invalid Bland webhook signatures. The pre-call handler is designed to degrade safely; direct users of verifyBlandSignature() can throw or map this error according to their own policy.
Mnemix client errors are not wrapped unless the kit needs to add Bland-specific context.
Configuration
| Option | Required | Description |
| --- | --- | --- |
| mnemix | Yes | A Mnemix client constructed with your API key. |
| blandWebhookSecret | No, recommended | Shared secret used to verify Bland webhook signatures. |
| recallTimeoutMs | No | Maximum time to wait before falling back to default pre-call context. |
| logger | No | Logger with info, warn, and error; defaults to console. |
Public API Surface
POST /v1/recall_and_enrichfor pre-call voice memory and enrichment.POST /v1/calls/endfor post-call memory write-back.
The only other frozen public v1 endpoint is GET /v1/caller/{phone_number}; this kit does not call it.
Construct the client with baseUrl: process.env.MNEMIX_BASE_URL || undefined when you need an explicit origin. Empty or omitted values let @mnemix-ai/client use its default, https://mcp.mnemix.ai.
Privacy & Compliance
The kit requires E.164 caller numbers and redacts phone numbers in its own logs. If your agent sends transcripts or caller context to a model provider, redact or filter that data in your application before write-back.
Keep your Bland retention settings, transcript storage, and recording URLs aligned with your own consent, retention, and data-processing terms.
License
MIT
