#Private tools
#What a tool sees
| Placeholder | Value |
|---|---|
$auth.subject | The token's sub |
$auth.token | The raw JWT, to forward to your API |
$auth.claims.<name> | Any custom claim |
# tools/my_bookings.yml
description: The visitor's upcoming bookings.
when_to_use: When they ask about their own bookings.
access: private
request:
method: GET
base_url: $env.API_URL
path: /api/bookings
headers:
Authorization: Bearer $auth.token
respond:
data:
bookings: bookings
Your endpoint verifies the same JWT with the same public key, and scopes the
query to its sub.
#Verifying on your endpoint
Vatio verifies the token before it unlocks a private tool, but that does not protect your API: the endpoint is on the internet, and anyone can call it with a made-up header. Verify the forwarded token on every request and check:
- Signature, with the public half of the key you sign with, and the
algorithm pinned (
ES256orRS256), never read from the token's header. audis your workspace slug, so a token signed for another workspace is refused.exphas not passed.
Then take the user from sub, and from nothing the request body says. A token
that fails any check is a 401.
In Rails, vatio-sdk does all of it:
class Api::BookingsController < ApplicationController
include Vatio::Identity::Authentication
before_action :authenticate_vatio! # 401 unless the token verifies
def index
render json: Booking.where(user_id: vatio_subject)
end
end
If your app serves several workspaces, each with its own key, build a
Vatio::Identity::Config per workspace and return it from
vatio_identity_config in the controller; see
Several workspaces.
Anywhere else, any JWT library will do. In Node, with
jose:
import { importSPKI, jwtVerify } from "jose";
const key = await importSPKI(process.env.VATIO_IDENTITY_PUBLIC_KEY, "ES256");
app.get("/api/bookings", async (req, res) => {
const token = req.headers.authorization?.replace(/^Bearer /i, "");
try {
const { payload } = await jwtVerify(token, key, {
audience: "acme", // your workspace slug
algorithms: ["ES256"]
});
res.json(await bookingsFor(payload.sub));
} catch {
res.status(401).json({ error: "unauthorized" });
}
});
Nothing to verify when no token is sent: public tools carry no $auth. On
WhatsApp and Instagram a private tool sends the token the contact signed in
with (On WhatsApp and Instagram).
#Rules
Never take the user's id as a parameter. The model fills parameters, and it
can be talked into filling that one with someone else's id. A private tool
declaring user_id, customer_id, account_id, member_id, patient_id or
subject fails the deploy:
tool my_bookings: a private tool cannot take "user_id" as a parameter — the model
fills parameters and can be talked into filling that one with someone else's id.
Use $auth.subject, which comes from the verified token
Derive the user from the token instead. An endpoint that accepts an id has to remember to scope it every time, forever; one that reads the token cannot forget.
Other errors you may hit:
| Error | Fix |
|---|---|
access: private needs an auth: block | Add auth: with public_key: |
auth.public_key ... is a PRIVATE key | Point at identity.pub, not identity.pem |
auth.algorithm must be one of RS256... | Sign with RS256/ES256, not HS256 — a shared secret cannot live in a manifest |
access must be "public" or "private" | Scheme names are gone; use private |
auth/ holds JavaScript auth providers, which are gone | Delete auth/, sign a JWT instead |
JavaScript tools are gone | Delete tools/*.js and lib/*.js; describe the call in tools/<key>.yml |
A rejected token — wrong key, expired, wrong aud — leaves the visitor
anonymous rather than erroring, and the agent falls back to its public tools.
vatio chat shows the reason.