Other languages
Publish and verify Varis services in any language, without an SDK.
Varis has an SDK for TypeScript and JavaScript. You can use Varis from any other language, such as Python, Go, Ruby, PHP, or Java. Without an SDK, you do two things by hand:
- Describe your services in
varis.json. - Verify requests from Varis in your endpoint.
Everything else works the same, including the CLI, the dashboard, and payouts.
Describe your services in varis.json
-
Install the CLI. For more information, see Install the CLI.
-
From your project folder, set up the project:
varis init --base-url https://api.example.com -
Add your services to the
serviceslist invaris.json. Write the input and output as JSON Schema:varis.json { "owner_id": "var_ownr_LOqdTCny9lP7CR", "base_url": "https://api.example.com", "services": [ { "slug": "convert-currency", "name": "Convert currency", "description": "Converts an amount from one currency to another at the current exchange rate.", "service_type": "data", "categories": ["finance", "forex"], "endpoint_url": "https://api.example.com/currency/convert", "method": "GET", "price_cents": 10, "input_schema": { "type": "object", "properties": { "from": { "type": "string", "description": "ISO 4217 code, such as USD." }, "to": { "type": "string", "description": "ISO 4217 code, such as EUR." }, "amount": { "type": "number" } }, "required": ["from", "to", "amount"] }, "output_schema": { "type": "object", "properties": { "rate": { "type": "number" }, "converted": { "type": "number" } }, "required": ["rate", "converted"] } } ] } -
Publish:
varis publish
Without an SDK, you write endpoint_url in full. The path shortcut only works when an SDK builds varis.json for you.
For every field, see the varis.json reference. For the full API contracts, see the API Reference.
Verify signatures without an SDK
Varis signs every request with an Ed25519 key. To check that a request comes from Varis, your endpoint rebuilds the signed text and checks the signature against the Varis public key.
What Varis sends
| Header | Value |
|---|---|
X-Varis-Signature | The Ed25519 signature, base64-encoded. |
X-Varis-Timestamp | When Varis signed the request, in whole seconds since the Unix epoch. |
X-Varis-Key-Id | The ID of the key that signed it. |
X-Varis-Request-Id | The ID of the call. It isn't signed. |
What Varis signs
Varis signs the UTF-8 bytes of this text:
{timestamp}.{method}.{path_and_query}.{raw_body}| Part | Value |
|---|---|
timestamp | The X-Varis-Timestamp header, exactly as sent. |
method | GET or POST. |
path_and_query | The path and query string exactly as received, still percent-encoded, without the host. For example, /currency/convert?amount=100&from=USD&to=EUR. |
raw_body | The request body as raw bytes. Empty for GET. |
Use the path, query, and body exactly as they arrived. Don't parse and re-encode them. Re-encoding JSON or reordering query parameters changes the bytes, and the signature fails.
Where the keys are
Fetch the public keys from:
https://api.usevaris.com/.well-known/varis-signing-keysThe response lists each key with its ID and an SPKI public key in PEM format:
{
"keys": [
{
"kid": "KEY_ID",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----\n"
}
]
}Check a request
To verify a request, do the following:
- Read
X-Varis-Signature,X-Varis-Timestamp, andX-Varis-Key-Id. If any is missing, reject the request. - Check that the timestamp is within 300 seconds of your server's clock. If it isn't, reject the request.
- Find the public key whose
kidmatchesX-Varis-Key-Id. Cache the keys, and refetch them when you see an unknown key ID, at most once every few minutes. - Build the signed text from the timestamp, method, path and query, and raw body.
- Verify the base64-decoded signature against the signed text with the public key.
If any step fails, respond with 401. If you can't fetch the keys, respond with 503, because the problem is on your side, not the caller's.
Examples
Each example has a function that verifies a request, and a route that calls it before doing anything else. Choose your language:
This example uses the requests and cryptography packages:
import base64
import time
import requests
from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.serialization import load_pem_public_key
KEYS_URL = "https://api.usevaris.com/.well-known/varis-signing-keys"
MAX_SKEW_SECONDS = 300
REFETCH_SECONDS = 300
_keys = {}
_last_fetch = 0.0
def _load_keys():
response = requests.get(KEYS_URL, timeout=5)
response.raise_for_status()
return {
key["kid"]: load_pem_public_key(key["public_key_pem"].encode())
for key in response.json()["keys"]
}
def verify_varis_request(method, path_and_query, headers, raw_body):
"""Returns True if the request comes from Varis.
method: "GET" or "POST".
path_and_query: the path and query exactly as received, such as
"/currency/convert?amount=100&from=USD&to=EUR".
headers: the request headers, with lowercase names.
raw_body: the body as bytes, exactly as received. b"" for GET.
"""
global _keys, _last_fetch
signature = headers.get("x-varis-signature")
timestamp = headers.get("x-varis-timestamp")
kid = headers.get("x-varis-key-id")
if not signature or not timestamp or not timestamp.isdigit():
return False
if abs(time.time() - int(timestamp)) > MAX_SKEW_SECONDS:
return False
# An unknown key ID can mean Varis rotated its keys. Refetch, but not
# more than once every few minutes, so forged requests can't make every
# call hit the network.
if kid not in _keys and time.time() - _last_fetch > REFETCH_SECONDS:
_last_fetch = time.time()
_keys = _load_keys()
key = _keys.get(kid)
if key is None:
return False
signed = f"{timestamp}.{method}.{path_and_query}.".encode() + raw_body
try:
key.verify(base64.b64decode(signature), signed)
return True
except (InvalidSignature, ValueError):
return FalseIn a Flask route:
from flask import Flask, abort, jsonify, request
from varis_verify import verify_varis_request
app = Flask(__name__)
@app.get("/currency/convert")
def convert():
# The path and query as the client sent them, before Flask decodes them.
path_and_query = request.environ.get("RAW_URI") or request.full_path.rstrip("?")
if not verify_varis_request("GET", path_and_query, request.headers, request.get_data()):
abort(401)
# Your logic here.
return jsonify(rate=0.92, converted=92)request.headers in Flask matches header names in any case. RAW_URI is set by Gunicorn and the Flask development server. If your server doesn't set it, make sure whatever you use keeps the path exactly as received.
Check your code with test vectors
Before you take real traffic, check your verification code against these values. They use a sample key, not a real Varis key.
Public key:
-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAA6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg=
-----END PUBLIC KEY-----Both requests use the timestamp 1760227200. Set your clock check to that time, or skip it while you test.
GET request:
| Part | Value |
|---|---|
| Signed text | 1760227200.GET./currency/convert?amount=100&from=USD&to=EUR. |
| Signature | 7lkSPdp78Bvn+k/bCJxk8FcYSIYwi05BbreVVRVFT41dOxxUCQ9HCAVsunSCz7FjRQTTNxvx6bITWRUFIemzAg== |
POST request:
| Part | Value |
|---|---|
| Signed text | 1760227200.POST./currency/convert.{"amount":100,"from":"USD","to":"EUR"} |
| Signature | c/aE1ylAC5IYa3nWWYPBg987NJyvHYfmvjH4lLC2P6FfglxuUVDdBH/7967aB9EdmuZY0mgU6JRIECgSJs9HAQ== |
Your code must accept both. Change one character of either signed text, and your code must reject it.
What's next
- Handle requests: what Varis sends and what it expects back.
varis.jsonreference.