VarisDocs
Dashboard

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:

  1. Describe your services in varis.json.
  2. 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

  1. Install the CLI. For more information, see Install the CLI.

  2. From your project folder, set up the project:

    varis init --base-url https://api.example.com
  3. Add your services to the services list in varis.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"]
          }
        }
      ]
    }
  4. 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

HeaderValue
X-Varis-SignatureThe Ed25519 signature, base64-encoded.
X-Varis-TimestampWhen Varis signed the request, in whole seconds since the Unix epoch.
X-Varis-Key-IdThe ID of the key that signed it.
X-Varis-Request-IdThe 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}
PartValue
timestampThe X-Varis-Timestamp header, exactly as sent.
methodGET or POST.
path_and_queryThe path and query string exactly as received, still percent-encoded, without the host. For example, /currency/convert?amount=100&from=USD&to=EUR.
raw_bodyThe 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-keys

The 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:

  1. Read X-Varis-Signature, X-Varis-Timestamp, and X-Varis-Key-Id. If any is missing, reject the request.
  2. Check that the timestamp is within 300 seconds of your server's clock. If it isn't, reject the request.
  3. Find the public key whose kid matches X-Varis-Key-Id. Cache the keys, and refetch them when you see an unknown key ID, at most once every few minutes.
  4. Build the signed text from the timestamp, method, path and query, and raw body.
  5. 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:

varis_verify.py
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 False

In 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:

PartValue
Signed text1760227200.GET./currency/convert?amount=100&from=USD&to=EUR.
Signature7lkSPdp78Bvn+k/bCJxk8FcYSIYwi05BbreVVRVFT41dOxxUCQ9HCAVsunSCz7FjRQTTNxvx6bITWRUFIemzAg==

POST request:

PartValue
Signed text1760227200.POST./currency/convert.{"amount":100,"from":"USD","to":"EUR"}
Signaturec/aE1ylAC5IYa3nWWYPBg987NJyvHYfmvjH4lLC2P6FfglxuUVDdBH/7967aB9EdmuZY0mgU6JRIECgSJs9HAQ==

Your code must accept both. Change one character of either signed text, and your code must reject it.

What's next