Identity flows
Every channel ends in the same place: a JWT signed with your private key, verified by Vatio with your public key, and forwarded to your API as $auth.token when a private tool runs. What changes per channel is how that JWT reaches Vatio.
| Channel | How the JWT arrives | Who asks |
|---|---|---|
| Web | Your page renders it onto the widget | Nobody; your page hands it over |
| Your mint endpoint returns it for a phone number | Vatio | |
| Your mint endpoint returns it for an Instagram account | Vatio |
The diagrams below are Mermaid so that they read the same in a browser, in the Markdown twin of this page and to a coding agent.
Keys and credentials
| Credential | Signed by | Verified by | Lives | Used on |
|---|---|---|---|---|
Identity JWT ($auth.token) | Your identity.pem | Vatio, with the identity.pub in vatio.yml | The exp you choose | Web, WhatsApp, Instagram |
Vatio-Signature | Vatio's mint key (ES256) | Your backend, with /.well-known/vatio-mint-key | 60 seconds | WhatsApp, Instagram |
Mint shared secret (X-Api-Key) | Nobody; it is a string | Your backend, in constant time | Until you rotate it | WhatsApp, Instagram |
Publishable token (vatpub_…) | Issued by Vatio | Vatio, plus the origin allowlist | Until revoked | Web |
chat_token | Vatio | Vatio | 12 hours | Web |
$auth.token is always the identity JWT. The publishable token names the workspace, not the person, and never reaches your backend.
Web
mermaid
sequenceDiagram
autonumber
participant B as Your backend
participant W as Widget / SDK
participant V as Vatio
participant A as Your API
B->>W: page with data-token=vatpub_… and data-visitor-token=JWT
W->>V: POST /api/public/v1/:ws/chats (Bearer vatpub_, visitor_token)
V->>V: origin allowed, rate limit, verify JWT with identity.pub
V->>V: store the JWT, write name/email/phone_number onto the contact
V-->>W: chat_token (12 h)
W->>V: messages (Bearer chat_token)
V->>V: private tool: verify the stored JWT again (exp)
V->>A: Authorization: Bearer $auth.token
A->>A: verify with your key- A visitor who signs in mid-conversation: the page calls
VatioWidget.identify(jwt), and the SDK posts it to/api/public/v1/:ws/chats/:id/identify. - A JWT with a different
subanswersidentity_changed, and the SDK starts a new conversation. One person never sees another's. - No JWT means an anonymous chat. Private tools answer that they could not confirm who the visitor is.
- The web never mints. See On the web.
WhatsApp
mermaid
sequenceDiagram
autonumber
participant C as WhatsApp contact
participant V as Vatio
participant B as Your backend (mint endpoint)
participant A as Your API
C->>V: message (Meta delivers the wa_id)
Note over V: a stored JWT that still verifies is reused, no mint
V->>V: body {"channel":"whatsapp","phone_number":"+569…"}
V->>V: sign Vatio-Signature (ES256, exp 60 s, body_sha256)
V->>B: POST auth.mint.url (X-Api-Key, Vatio-Signature)
B->>B: shared secret, then signature (iss, aud = slug, body hash)
B->>B: look the number up, exactly as sent
alt known
B-->>V: 200 {"token": JWT signed with identity.pem}
V->>V: verify with identity.pub, store on the chat
V->>A: private tool: Authorization: Bearer $auth.token
else unknown, or any check failed
B-->>V: 404 {}
Note over V: the chat stays anonymous
end- Mint runs when a chat starts and whenever a private tool runs without a JWT that still verifies, so the next one happens after
exp. phone_numbercomes from the number Meta delivered, never from a token claim written onto the contact.- A user who hides their number behind a WhatsApp username carries no phone, and no request is sent.
Instagram
mermaid
sequenceDiagram
autonumber
participant C as Instagram contact
participant M as Meta Graph API
participant V as Vatio
participant B as Your backend (mint endpoint)
participant A as Your API
C->>V: DM (Meta delivers the IGSID)
Note over V: a stored JWT that still verifies is reused, no mint
V->>M: current username for this IGSID
M-->>V: username (or no answer)
V->>V: body {"channel":"instagram","instagram_id":"1784…","username":"ana.perez"}
V->>V: sign Vatio-Signature (ES256, exp 60 s, body_sha256)
V->>B: POST auth.mint.url (X-Api-Key, Vatio-Signature)
B->>B: shared secret, then signature (iss, aud = slug, body hash)
B->>B: look up by instagram_id, or by a verified username
alt known
B-->>V: 200 {"token": JWT signed with identity.pem}
V->>V: verify with identity.pub, store on the chat
V->>A: private tool: Authorization: Bearer $auth.token
else unknown, or any check failed
B-->>V: 404 {}
Note over V: the chat stays anonymous
endinstagram_idis the IGSID: stable, and unique to this person for your account. It is the field to match on once you have linked it.usernameis asked from Meta for every mint and left out when Meta does not answer. Instagram usernames can be changed and reused, so match on one only if you verified it belongs to the user.- The request is the same endpoint as WhatsApp. Branch on
channel.
Where to go next
- Sessions and channels: the request and response shapes, and what to verify on the mint request.
- Private tools:
access: privateand the$auth.*placeholders. vatio-identity: the Ruby gem that signs the JWT and serves the mint endpoint.
