Your support inbox knows the email. It doesn't know the customer.
Customer Context closes that gap. On every new ticket, ElkDesk pulls a few live facts from your own backend — subscription status, plan, credits, whatever you choose — and shows them in the ticket sidebar. The same facts get fed to the AI before it drafts a reply, so the suggestion already knows whether this person is on a paid plan or out of credits.
It's read-only, it's a Business-plan feature, and there's zero per-customer hardcoding on ElkDesk's side. You describe, in plain English, how to find the customer in the email and what to show. ElkDesk does the rest by calling one endpoint you host.
This guide walks an engineer through both halves: the endpoint you build on your backend, and the settings you fill in on ElkDesk.
How it works
When a new (non-spam) ticket arrives, ElkDesk runs three steps:
- Extract an identifier. An AI step reads the email subject and body and pulls out one lookup value, following your plain-language instructions (e.g. "the account ID is in the body after
Account ID:"). - Call your endpoint. ElkDesk sends that identifier to the one URL you configured, with a secret bearer token in the
Authorizationheader. Your endpoint returns JSON. - Summarize. A second AI step turns your JSON into a short summary plus labelled rows, following your "what to show" instructions.
The result is stored on the ticket: rendered in the sidebar, and given to the AI as background context when it analyzes the ticket and drafts a reply. It's fetched once, when the ticket arrives, then reused for follow-ups, regenerated suggestions, and rewrites.
A couple of things worth knowing up front:
- The AI only ever produces two things: the identifier and the summary. It never chooses the URL, method, headers, or token — you configure those.
- If a step fails (no identifier, your API errors, a bad response), the ticket and the normal AI analysis carry on — the sidebar just shows a short status line.
What you'll need
- An ElkDesk project on the Business plan, and you must be the project owner (members can't see or change this).
- A read-only HTTPS endpoint on your backend that looks a customer up by an identifier.
- About 30 minutes.
Step 1 — Build a read-only endpoint on your backend
This is the only part that lives in your codebase. ElkDesk calls it; it returns JSON about one customer.
The contract ElkDesk requires
Your endpoint has to satisfy all of these, or the lookup is rejected:
| Requirement | Detail |
|---|---|
| HTTPS only | Plain http:// is rejected. |
| Publicly reachable | ElkDesk calls it over the internet, so localhost or internal-only hosts won't work. |
| Fast | Respond within 5 seconds, or the request times out. |
| Small | Response body must be under 64 KB. |
| JSON | Content-Type must be application/json (or a +json type). |
| No redirects | A 3xx response is treated as an error. |
| No credentials in the URL | Put the secret in the header (ElkDesk does this for you), not in the URL. |
What ElkDesk sends
With GET (the simplest option), it appends your identifier as a query parameter:
GET https://api.yourapp.com/support-context?id=acct_12ab34cd
Authorization: Bearer THE-SHARED-SECRET
Accept: application/json
With POST, it sends a JSON body instead:
POST https://api.yourapp.com/support-context
Authorization: Bearer THE-SHARED-SECRET
Accept: application/json
Content-Type: application/json
{ "id": "acct_12ab34cd" }
id here is whatever you name the identifier parameter in settings. The identifier value is sanitized before it's sent — only A-Za-z0-9 . _ @ : - survive, capped at 256 characters — so emails, UUIDs, and prefixed IDs all pass through cleanly, and anything stranger never reaches you.
What to return
Any JSON object. ElkDesk's summarizer reads it together with your "what to show" instructions, so you don't need a fixed schema — return what's useful:
{
"type": "customer",
"plan": "Pro",
"status": "active",
"seats": 5,
"mrr": 49,
"signedUpAt": "2025-09-01"
}
Two optional extras:
- Include a
typefield if your endpoint can return different shapes (see Handling more than one kind of ID). It helps the summarizer label things correctly. - If you return a
linksarray of objects shaped likelabelandurl, those render as clickable links in the sidebar (onlyhttp/httpsURLs are kept). Handy for a "View in admin" deep link.
For an unknown identifier, return 200 with an empty JSON object — not a 404. ElkDesk treats that as a clean "no data found" rather than an error.
Reference implementation (Next.js route handler)
// app/api/support-context/route.js — on YOUR backend
import { NextResponse } from "next/server";
import { timingSafeEqual } from "node:crypto";
function authorized(request) {
const expected = process.env.ELKDESK_SUPPORT_TOKEN || "";
const got = (request.headers.get("authorization") || "").replace(/^Bearer\s+/i, "");
if (!expected || !got) return false;
const a = Buffer.from(got);
const b = Buffer.from(expected);
// constant-time compare; guard length first (timingSafeEqual throws on mismatch)
return a.length === b.length && timingSafeEqual(a, b);
}
export async function GET(request) {
if (!authorized(request)) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const id = (new URL(request.url).searchParams.get("id") || "").trim();
if (!id) return NextResponse.json({}); // nothing to look up
// Read-only lookup. Use your ORM / parameterized query — never string-build SQL.
const customer = await db.customer.findUnique({
where: { accountId: id },
select: { plan: true, status: true, seats: true, mrr: true, createdAt: true },
});
if (!customer) return NextResponse.json({}); // unknown id -> empty, not 404
return NextResponse.json({
type: "customer",
plan: customer.plan,
status: customer.status,
seats: customer.seats,
mrr: customer.mrr,
signedUpAt: customer.createdAt,
});
}
The Express or Fastify shape is the same: check the bearer token, read the id, do one read-only query, return JSON.
Security checklist (do all of these)
- Verify the bearer token on every request with a constant-time compare; return
401if it's missing or wrong. - Keep it read-only — this endpoint should never write, update, or delete.
- Validate the identifier's format before querying, and use parameterized queries or your ORM (no SQL injection).
- Return the minimum fields your agents need. No password hashes, no tokens, no full payment details.
- Store the secret in an environment variable, and be ready to rotate it.
The endpoint is already bearer-gated and ElkDesk calls it only once per ticket, so you don't need heavy rate-limiting. If you want defense-in-depth in case the token ever leaks, a light limit caps how fast anyone could enumerate records — but it's optional.
Step 2 — Generate the shared secret
Create a strong random token. You'll put the same value in two places: your backend's environment, and ElkDesk's settings.
openssl rand -base64 32
Set it on your backend (for example as ELKDESK_SUPPORT_TOKEN) and redeploy so your endpoint can check it.
Step 3 — Configure ElkDesk
Go to Settings → Customer Context (you'll only see it as a Business-plan owner). Fill in:
| Field | What to enter |
|---|---|
| Method | GET or POST — match what your endpoint accepts. |
| API URL | The full HTTPS URL of your endpoint, e.g. https://api.yourapp.com/support-context. |
| Identifier parameter | The query or body key your endpoint reads, e.g. id. |
| Bearer token | Paste the secret from Step 2. It's encrypted at rest; once saved it shows as configured, and you can leave it blank to keep the existing one. |
| How to identify the customer | Plain-language instructions for finding the lookup value in the email. Example: "The account ID is in the body after Account ID: and starts with acct_." |
| What to show in the sidebar | Plain-language instructions for which facts to surface, and their labels. Example: "Show Plan; Status (Active or Inactive); Seats; Monthly revenue." |
| Enable customer context | Turn this on once you've saved and tested. |
That's it — no code on ElkDesk's side, no per-customer rules.
Handling more than one kind of ID
You'll notice there's only one identifier parameter field. That's deliberate, and it trips people up, so here's the pattern.
ElkDesk extracts one identifier per ticket and sends it under that one parameter name. You don't configure a second one. Instead, when your emails can carry different kinds of ID, you:
- Tell the extractor to grab whichever is present. In How to identify the customer:
"If the body contains
Order #followed by a number, use that order number. Otherwise, if the body has anacct_account ID, use that." - Use one generic parameter name like
id— both kinds travel through it. - Let your endpoint detect the kind from the value's shape and return the matching JSON:
if (/^acct_[a-z0-9]+$/i.test(id)) {
// account lookup -> { type: "account", plan, status, ... }
} else if (/^ORD-\d+$/.test(id)) {
// order lookup -> { type: "order", item, amount, fulfillment, ... }
}
- Cover both in "what to show":
"If it's an account, show Plan and Status. If it's an order, show Item, Amount, and Fulfillment status."
The summarizer formats whichever shape comes back. The one limit: if a single email contains both kinds of ID, only the one your instructions prioritize gets looked up — there's one lookup per ticket. In practice your email types are usually distinct, so this is fine.
Step 4 — Test before you enable
The settings page has a Test connection panel. Enter a realistic sample subject and body, and run it. ElkDesk performs the full extract, fetch, and summarize cycle once and shows you:
- the identifier it pulled from your sample,
- the status,
- the summary and fields exactly as they'll appear in the sidebar.
If the identifier is right but fields are empty, your endpoint returned no data for that value (or the "what to show" instructions don't match your JSON). If the status is an error, check the table below.
Your token stays private
- Encrypted at rest, and decrypted only when ElkDesk calls your endpoint.
- Never sent to the AI, and never written to logs.
- Shown only as "configured" after you save it — the settings screen never displays the value again.
Troubleshooting: status meanings
The sidebar shows a status when a lookup doesn't fully succeed:
| Status | Meaning |
|---|---|
ok | Identifier found, your API answered, summary rendered. |
no_identifier | The AI couldn't find a lookup value in the email. Tighten your How to identify instructions. No call was made to your API. |
fetch_error | Your endpoint timed out, returned non-2xx, redirected, returned non-JSON, or was too large. Check the contract in Step 1. |
skipped_credits | The project's monthly AI credits were exhausted, so the lookup was skipped. |
disabled | The connection isn't enabled, or the plan no longer includes Customer Context. |
Once ok shows up in the test panel with the right fields, switch Enable customer context on, and the next new ticket will carry live data into the sidebar and the AI's reply.


