Overview
This guide is for a tenant whose custom skill calls POST /agent-tooling/actions with type http and needs the customer's WhatsApp number to look up the right account, order, or booking.
Do not trust
A model-filled phone field
Anything in an action input schema is something the model can invent and a customer can try to change in chat.
Trust after verify
The signed identity token
Drusy mints the token from the channel delivery record, signs it, and sends it outside the model-controlled tool arguments.
Authorization is left alone. That header belongs to the auth scheme you configured for the action. X-Drusy-Identity is about the customer.Prompt awareness
The assistant also sees the caller's number in per-turn context so it stops asking for it or guessing it. That context improves the conversation, but it is not how your backend receives the number.
Customer:
- WhatsApp number: +2348012345678 (verified by the channel)
- Name: Ada Okonkwo
Their number is already attached to every tool call automatically. Never ask
for it, and never put it in a tool argument unless a field explicitly asks
for a different person's number.The token
When the caller phone number is known, every outbound custom HTTP action request includes one signed JWT header.
X-Drusy-Identity: eyJhbGciOiJFUzI1NiIsImtpZCI6ImsxIiwidHlwIjoiSldUIn0...sub+2348012345678The customer's phone number, in E.164.
isshttps://drusy.aiAlways this. Pin it.
audhttps://api.example.comThe origin Drusy sent the request to. Pin your own origin.
exp / iat1754300300Five-minute lifetime.
jti9f2c...Unique per request, for an optional replay cache.
biz507f1f77bcf86cd799439011The Drusy business this request belongs to.
actlookup_orderThe action that fired, for optional per-action checks.
bhn4bQgYhMfW...base64url SHA-256 of the raw request body, when there is one.
Getting your keys
Each business gets its own ECDSA P-256 keypair. Drusy holds the private key. Your server uses the public key through JWKS.
GET https://api.drusy.ai/public/signing-keys/:businessId/jwks.json{
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "...",
"y": "...",
"kid": "k1",
"alg": "ES256",
"use": "sig"
}
]
}kid so your library can pick the right key during rotation.Verify the JWT
Reject the request unless signature verification passes. A decoded but unverified token is just attacker-controlled text.
Verifier checklist
- Use verify, not decode
- Pin issuer to https://drusy.ai
- Pin audience to your own origin
- Pin algorithms to ES256
- Use the JWKS URL so key rotation keeps working
createRemoteJWKSet caches keys and refetches when it sees an unknown kid.
import { createRemoteJWKSet, jwtVerify } from "jose";
const JWKS = createRemoteJWKSet(
new URL("https://api.drusy.ai/public/signing-keys/<businessId>/jwks.json"),
);
async function verifyDrusyIdentity(req) {
const { payload } = await jwtVerify(req.get("x-drusy-identity"), JWKS, {
issuer: "https://drusy.ai",
audience: "https://api.example.com",
algorithms: ["ES256"],
});
return payload.sub;
}
app.post("/drusy/lookup-customer", async (req, res) => {
let phone;
try {
phone = await verifyDrusyIdentity(req);
} catch {
return res.sendStatus(401);
}
// Look the customer up by phone.
});Body hash
The optional bh claim is base64url SHA-256 of the raw request bytes. Compute it over bytes as received, not over re-serialized JSON.
import crypto from "node:crypto";
const bh = crypto
.createHash("sha256")
.update(req.rawBody)
.digest("base64url");
if (payload.bh && payload.bh !== bh) {
return res.sendStatus(401);
}bh still leaves you with an authentic phone number. Check it when you also need proof that the payload was not changed.Am I actually verifying?
The action test sheet sends a normal request, then can send a second request with a structurally valid JWT whose signature has been corrupted.
Bad signature
Your endpoint should reject it.
Fake phone
The probe uses +10000000000, never a real customer.
Marked test
The request includes X-Drusy-Test: true for logs.
Rotation
Rotate from the dashboard or API. Drusy retires the current key and mints a new one immediately. Retired keys stay in JWKS so in-flight tokens keep verifying.
POST /identity/business/signing-key/rotate with SETTINGS_WRITEGo-live and limits
Before you ship
- Send customer identity is on only for endpoints you control
- Your server verifies X-Drusy-Identity before any customer lookup
- Verifier pins iss, aud, and algorithms
- JWKS URL is configured with the right business id
- Invalid-signature probe returns an error
- Body hash check is added if you need payload binding
What this does not cover
- An endpoint that decodes a JWT without verifying the signature
- Replay inside the five-minute window at the same endpoint, unless you cache jti
- Cross-action replay within one origin, unless you check act
- Compromise of Drusy's own signing infrastructure