Account lookup
Let Hobson look up a customer's account in your own app, with a signed request to an endpoint you host.
If you run a SaaS product, the useful context isn't an order, it's the customer's account: their plan, whether they're active, and whether something is going wrong for them right now. Account lookup lets Hobson ask your app for that.
You host one HTTPS endpoint. When a customer who has proved who they are gets in touch, Hobson sends it the customer's email address (or the user id your chat widget passed in) and your endpoint answers with a short summary of the account. Your team sees it beside the conversation, and Hobson uses it when it drafts a reply. The lookup only reads: it never changes anything in your app. Actions you add on the same settings page are separate and only run when a teammate approves them.
Set it up
- In Hobson, open Settings › Account lookup. Only owners and admins can see and change it.
- Enter your endpoint's URL. It must be a public
https://address on the standard port. - Copy the signing secret. It starts with
hsk_and Hobson shows it once. - Select Send test to send a lookup for an email address you choose and see exactly what came back.
The request
Hobson sends a POST with a JSON body:
POST https://api.example.com/hobson/lookup
Content-Type: application/json
X-Hobson-Timestamp: 1791590400
X-Hobson-Signature: v1=5f2b0c…
X-Hobson-Request-Id: 0b6f3a8e-0f4d-4c5e-9d8a-2d1f3c4b5a69
{
"version": 1,
"workspace": "ws_8Kq2…",
"customer": { "email": "ada@example.com", "externalId": "usr_123" },
"reason": "ticket"
}customerhasemail,externalIdor both.externalIdis only sent for live chat visitors your widget identified with a verified user id.reasonisticketfor a real conversation andtestwhen someone selects Send test in Settings.X-Hobson-Request-Idis unique to each request. Log it if you want to match a lookup to your own logs.
Check the signature
The signature is a hex HMAC-SHA256 of the timestamp, a full stop and the raw request body, keyed with your secret:
v1=hex(HMAC_SHA256(secret, timestamp + "." + body))- Compute it over the raw body exactly as received, before parsing the JSON.
- Reject requests whose timestamp is more than 5 minutes from your clock.
- After you generate a new secret, Hobson signs with both the new and the old secret for 24 hours, so the header holds two values separated by a comma:
v1=…,v1=…. Accept the request if either matches.
Your answer
Answer 200 with JSON. Only found is required:
{
"found": true,
"name": "Ada Lovelace",
"plan": "Pro",
"status": "active",
"mrrMinor": 4900,
"currency": "GBP",
"signedUpAt": "2025-03-14",
"lastSeenAt": "2026-10-06T09:12:00Z",
"flags": [{ "label": "Auth errors 24h", "tone": "danger" }],
"fields": [{ "label": "Seats", "value": "12 of 15" }],
"adminUrl": "https://admin.example.com/users/usr_123"
}| Field | Format |
|---|---|
found | true or false. Answer { "found": false } when there's no such customer. |
name | Up to 120 characters |
plan | Up to 60 characters |
status | Up to 30 characters, such as active, trialing or past_due |
mrrMinor | Whole number in minor units, so 4900 is £49.00 |
currency | Three capital letters, such as GBP |
signedUpAt, lastSeenAt | An ISO date or date-time with a time zone |
flags | Up to 8, each a label (40 characters) and a tone of neutral, warning or danger |
fields | Up to 12, each a label (40 characters) and a value (200 characters) |
adminUrl | An https:// link to the customer in your admin |
A null field is treated as missing. The answer must be under 64 KB.
Example endpoint
A Node.js endpoint using Express:
import crypto from 'node:crypto';
import express from 'express';
const secrets = [process.env.HOBSON_LOOKUP_SECRET];
const app = express();
app.post('/hobson/lookup', express.raw({ type: 'application/json' }), async (req, res) => {
const timestamp = req.get('x-hobson-timestamp') ?? '';
const signatures = (req.get('x-hobson-signature') ?? '').split(',').map((part) => part.replace(/^v1=/, ''));
const body = req.body.toString('utf8');
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(401);
const valid = secrets.some((secret) => {
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
return signatures.some((signature) => signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)));
});
if (!valid) return res.sendStatus(401);
const { customer } = JSON.parse(body);
const user = await findUser({ email: customer.email, id: customer.externalId });
if (!user) return res.json({ found: false });
res.json({
found: true,
name: user.name,
plan: user.plan.name,
status: user.subscriptionStatus,
mrrMinor: user.plan.priceMinor,
currency: 'GBP',
signedUpAt: user.createdAt.toISOString(),
flags: user.recentAuthErrors > 0 ? [{ label: 'Auth errors 24h', tone: 'danger' }] : [],
adminUrl: `https://admin.example.com/users/${user.id}`,
});
});Next.js route handler
Save this as app/hobson/lookup/route.ts and set HOBSON_LOOKUP_SECRET. Replace findUser with your own query.
import crypto from 'node:crypto';
const secrets = [process.env.HOBSON_LOOKUP_SECRET!];
function verify(timestamp: string, header: string, body: string) {
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const signatures = header.split(',').map((part) => part.trim().replace(/^v1=/, ''));
return secrets.some((secret) => {
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${body}`).digest('hex');
return signatures.some(
(signature) => signature.length === expected.length && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected)),
);
});
}
export async function POST(request: Request) {
const body = await request.text();
const timestamp = request.headers.get('x-hobson-timestamp') ?? '';
const signature = request.headers.get('x-hobson-signature') ?? '';
if (!verify(timestamp, signature, body)) return new Response(null, { status: 401 });
const { customer } = JSON.parse(body) as { customer: { email?: string; externalId?: string } };
const user = await findUser({ email: customer.email, id: customer.externalId });
if (!user) return Response.json({ found: false });
return Response.json({
found: true,
name: user.name,
plan: user.planName,
status: user.subscriptionStatus,
signedUpAt: user.createdAt.toISOString(),
adminUrl: `https://admin.example.com/users/${user.id}`,
});
}Try it with curl
Sign a test request with your secret and send it to your endpoint before you save the URL in Hobson:
SECRET=hsk_your_secret
BODY='{"version":1,"workspace":"ws_test","customer":{"email":"ada@example.com"},"reason":"test"}'
TS=$(date +%s)
SIG=$(printf '%s.%s' "$TS" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
curl -X POST https://api.example.com/hobson/lookup \
-H 'Content-Type: application/json' \
-H "X-Hobson-Timestamp: $TS" \
-H "X-Hobson-Signature: v1=$SIG" \
-d "$BODY"A working endpoint answers with JSON such as {"found":true,"name":"Ada Lovelace","plan":"Pro","status":"active"}. A 401 means the signature or timestamp check failed.
What Hobson does with it
- Your team sees the summary in the conversation's details panel, with a link to
adminUrl. - Hobson reads the summary when it drafts, so a reply can take the customer's plan and recent problems into account. It never sees
adminUrl, and the link never reaches the customer. - Linear issues raised from the conversation include the plan, status and flags, never the customer's email.
Hobson only looks up a customer it can trust: an email conversation, or a live chat whose visitor verified their email or was identified by your widget. Conversations flagged as a possible prompt injection are never looked up.
Timing, caching and limits
- Your endpoint has 3 seconds to answer.
- A found account is cached for 10 minutes and a miss for 2 minutes. Your team can refresh it from the conversation.
- Hobson makes at most 600 lookups an hour per workspace.
- After 5 failed lookups in a row, Settings shows the lookup as failing. The next successful lookup clears it.
- Settings › Account lookup lists the last 20 requests Hobson made to your endpoint, with the time, whether it came from a conversation or a test, the HTTP status, how long it took and any error. Hobson never stores the request or response bodies.
- A
401or403from your endpoint usually means the signature check failed: make sure you're using the current secret.