The headers
Header names can arrive in any letter case, so match them case-insensitively.
When Pria calls your server itself (every conversation model except OpenAI models with native MCP support), each tool call is signed on its own, with a fresh nonce and timestamp. On OpenAI models with native MCP support the headers are fixed when Pria builds the tool list and reused for every call that list serves: once per reply in chat, and for the life of the voice session’s cached context in voice and realtime voice, so a long uninterrupted voice session can reach the 60-minute window. Several calls can therefore carry the same nonce, so do not reject a repeated nonce as a replay.
The rule
- Take the seven headers listed above that are present, except
x-pria-signature: six fields, or seven when the email is sent. Ignore any otherx-pria-*header. - Lowercase each header name and sort the fields by that name.
- Write each field as
name=valueand join them with a single newline (\n). No trailing newline. - Compute HMAC-SHA256 of that string (UTF-8) with the MCP Signing Secret as the key. Use the secret exactly as the text shown in the admin screen: do not hex-decode it.
- Accept the call only if
|now − x-pria-timestamp| ≤ 60 minutes, and the lowercase hex digest equalsx-pria-signature(compared in constant time).
x-pria-user-id, x-pria-institution-id, x-pria-server-label, x-pria-timestamp and x-pria-nonce must all be present. A request that carries only the email, user id and Digital Twin id, with no signature, was sent unsigned, for example because the twin has no signing secret yet. Treat it as unverified.
Verifiers
Both check staleness first and the signature second, the same order Pria’s own verifier uses, so the reason codes match. Each returns{ ok: true, keyId } or { ok: false, reason }.
Worked example
Run your verifier on this input before you point it at real traffic. The secret is a dummy: never use it for anything else. Signing secret (64 characters, used as text):856beaf60e86b85ebff5785af750ba845b5a55559400115c172ab1bfd1d92e5f, and the key id of this secret is a8ae6e6e.
The timestamp is September 16, 2025, so checked against today’s clock the example returns stale. That is correct. To reproduce the pass, give the verifier a clock one second after the timestamp: now: 1758000001000 in Node, now_ms=1758000001000 in Python. It then returns ok with key id a8ae6e6e. Change any header value and it returns bad-signature.
Which secret
Ask the Digital Twin’s administrator to share the MCP Signing Secret through a secure channel, not email.Rotation
Regenerating the MCP Signing Secret takes effect immediately. Pria never signs with the old and new secrets at the same time, so plan the change together with the twin’s administrator:- Pick a quiet hour and have the administrator regenerate the secret and send you the new value.
- Install the new secret. The new
x-pria-key-idtells you which calls it signed. - A realtime voice session that started before the change keeps the headers it began with. For the next 60 minutes, accept either secret, choosing by
x-pria-key-id, then remove the old one.
a8ae6e6e.
Related
- MCP Servers (admin): what the signed identity enables
- Connectors: connector configuration reference
- MCP Server: both MCP roles in Pria