Request Signing & Security
Every request VocoAgents makes to your endpoints — custom API tools, MCP tools, and webhooks — carries authentication headers so you can confirm the request genuinely came from us, and (optionally) that it is for your account specifically. This lets your endpoint reject anything that is unsigned or unrecognized.
The two headers
They come from two independent systems, so verifying both gives you defense-in-depth: forging a fully valid request would require compromising both, not one.
| Header | Proves | Sent | You verify with |
|---|---|---|---|
x-voco-signature | Genuinely from VocoAgents | Always | Our public key (below) |
x-voco-token | For your account | Only if your account has a secret | A shared secret we gave you privately |
Based on open standards
Both headers are standard JSON Web Tokens — no proprietary format. Any JWT library in any language can verify them. We follow these IETF specifications:
x-voco-signature
A short-lived RS256 JSON Web Token (JWT) signed with our private key. Because it is asymmetric, you verify it with our public key — which can only verify a signature, never create one, so it is safe for us to publish it here. This header is present on every request, and answers "is this genuinely from VocoAgents?"
Two claims answer two different questions:
- The signature itself proves who sent the request — VocoAgents, and no one else.
- The
aud(audience) claim — your account's opaque public id — proves who the request was for. It is present only when the agent is connected to an account. If we give you your account's public id, verify it too so you can confirm the request was meant for your account and not another.
Token claims
{
"iss": "vocoagents",
"iat": 1737000000,
"exp": 1737000300, // ~5 minute lifetime
"jti": "unique-token-id", // replay guard (reject a repeated jti to stop replay)
"aud": "account-public-id" // your account's public id — present only when the agent is on an account
}Our public key (RS256)
Copy this into your verifier. It is stable — we do not rotate it.
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA/dquG0Gyti3FkSlPOlqj
h5VJAdppsX2lo+WSKvsoD4Emlf4X4r/eC3EBg9iLoZ2c/XQOydNJ9PyaN8kaDvPU
c/99JsdgusfRRiiZahm8eWbFw5Foda3V2Mr62pjYdb+PluhjEh7neSZ+1vBhECgR
qRzANrEGICLhckJHK6dmZg1jkjrzz5Q/qLYs2u7K2gdrruvHq7pLvbo7HHs825MS
6Q1vSxNm9IRnqSQTItyqlmFnn+flCG5pZ9e+cCMmdRiP9z5RwmMGCpl7WH29iEL+
pTo2I23b+P5d8xs5PdOnUn9ImHA4AKU+0ZtlfHXz0/jNxjFtNmEVc2koXnvCkmL2
gwIDAQAB
-----END PUBLIC KEY-----Verify it (Node.js)
// Install: npm i jsonwebtoken
import jwt from 'jsonwebtoken';
const VOCO_PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
... paste the public key above ...
-----END PUBLIC KEY-----`;
app.post('/your-endpoint', (req, res) => {
try {
jwt.verify(req.headers['x-voco-signature'], VOCO_PUBLIC_KEY, {
algorithms: ['RS256'],
issuer: 'vocoagents',
audience: 'your-account-public-id', // optional: confirms it was for YOUR account
});
// exp / iss / aud are all checked by the library.
} catch {
return res.status(401).send('bad or missing signature');
}
// ...handle the request
});Retire each signature (recommended)
Every token carries a unique jti (JWT id). For single-use enforcement, store each jti you accept and reject any request whose jti you have already seen. This closes the replay window entirely: even within the token's short lifetime, a captured request cannot be sent twice. You only need to remember a jti until its exp passes — after that it can no longer verify, so old entries can be evicted.
const seen = new Map(); // jti -> exp (use Redis/DB in production)
function acceptOnce(payload) {
if (seen.has(payload.jti)) return false; // already used -> reject
seen.set(payload.jti, payload.exp);
return true;
}
// Verify first, then check single-use:
const claims = jwt.verify(req.headers['x-voco-signature'], VOCO_PUBLIC_KEY, {
algorithms: ['RS256'], issuer: 'vocoagents',
});
if (!acceptOnce(claims)) return res.status(401).send('replayed request');
// Optional: periodically drop entries whose exp has already passed.x-voco-token
A short-lived HS256 JWT signed with a private shared secret, unique to your account, that we hand you directly through your account manager (never over this page or a public channel). The raw secret is never sent — only the signed token is. When your account has a secret, we send this header on every request; you verify the token with the secret and reject anything that fails.
x-voco-token: eyJhbGciOiJIUzI1NiIsImtpZCI6Imtf…<signed JWT>Verify it (Node.js)
Verify with the shared secret we gave you — reject if it fails.
import jwt from 'jsonwebtoken';
try {
jwt.verify(req.headers['x-voco-token'], process.env.VOCO_TOKEN_SECRET, { algorithms: ['HS256'], issuer: 'vocoagents' });
} catch {
return res.status(401).send('bad or missing account token');
}Secret rotation (zero downtime)
When we rotate your secret, no request is ever rejected during the switch:
- We generate a new secret and share it with you — it is not in use yet.
- You add it to your verifier alongside your current secret (matched by the token's
kid), so both are accepted. - We swap over and start signing tokens with only the new secret.
- Once traffic is on the new secret, you remove the old one.
Recommended checks
Verify at least one header — we recommend both.x-voco-signature alone is the quickest to adopt: it needs no onboarding, just our public key (below), and is sent on every request. Verifying x-voco-token as well adds a second, independent layer — the two derive from different mechanisms (a public/private keypair vs. a per-account shared secret held in a separate system), so passing both is meaningfully stronger than either alone.
- Verify
x-voco-signatureagainst our public key, and reject on failure — this proves who sent the request. - Confirm the token is unexpired (the library checks
expfor you). - If we gave you your account's public id, verify the
audclaim matches it — this proves who the request was for (your account). - Retire each
jti(store the ones you have seen) to enforce single-use and block replays. - For the strongest posture, also verify
x-voco-tokenwith your shared secret (HS256) and require it. - Always serve your endpoints over HTTPS.
