On this page

#Private tools

#What a tool sees

PlaceholderValue
$auth.subjectThe token's sub
$auth.tokenThe 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 (ES256 or RS256), never read from the token's header.
  • aud is your workspace slug, so a token signed for another workspace is refused.
  • exp has 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:

ErrorFix
access: private needs an auth: blockAdd auth: with public_key:
auth.public_key ... is a PRIVATE keyPoint 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 goneDelete auth/, sign a JWT instead
JavaScript tools are goneDelete 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.

Last updated:

Latest CLI version: 0.79.0 · Changelog · The whole contract as Markdown: /docs/docs.md · or vatio docs