Skip to content

Getting Started — validate your first license in 5 minutes

This is the fastest path from a clean checkout to a validated license: run the server → create a product → create a plan → issue a license → validate it from an SDK. Everything below uses the public REST API and the client SDKs — the same surface a real integrator hits.

Why an SDK for the last step? License validation is a challenge/response: the server issues a geometric-proof challenge, the client solves it and submits the proof. The SDKs do this for you in one validate() call, so you don't hand-roll the proof. See Geometric Proof, explained.

0. Prerequisites

  • Rust 1.70+ (with cargo)
  • PostgreSQL 12+ reachable (the devcontainer already runs one on :5432)
createdb sawabona            # one-time

1. Run the server (migrations run automatically)

cd sawabona
SAWABONA_DATABASE__URL=postgresql://localhost:5432/sawabona \
  cargo run -p sawabona-api

Auth. Management endpoints (create product/plan/license) always require a key — there is no open mode. Send it as Authorization: ApiKey <key>; the header X-API-Key is not read. The key must be an admin key: being in SAWABONA_API_KEYS__KEYS gets you past the door, and these routes then check SAWABONA_API_KEYS__ADMIN_KEYS (or SAWABONA_ADMIN_API_KEY) as well.

config/development.toml ships two fixtures: test-operator-key, which is an admin key, and test-admin-key, which — despite its name — is a plain one. The commands below use the operator key and work against a local instance as written. Both are public in this repository, and the server refuses to start with either of them outside development.

A few routes are open by design because the licence key is itself the credential: /api/v1/licenses/validate, the geometric-proof challenge and proof, /heartbeat, and the public /api/v1/catalogue.

2. Create a product

curl -sX POST http://localhost:8000/api/v1/products \
  -H "Authorization: ApiKey test-operator-key" \
  -H "Content-Type: application/json" \
  -d '{"name":"My App","slug":"my-app","version":"1.0.0","description":"Demo product"}'

Copy the returned id (a UUID) → PRODUCT_ID.

The slug also becomes the product segment of every licence key, so it is limited to 1-8 characters, lowercase letters, digits and inner hyphens, no underscore — a key is six underscore-separated segments and an underscore here adds a seventh. For a longer name, pass a short key_slug beside it: {"name":"My Application","slug":"my-application","key_slug":"myapp", …}.

3. Create a plan for that product

curl -sX POST http://localhost:8000/api/v1/plans \
  -H "Authorization: ApiKey test-operator-key" \
  -H "Content-Type: application/json" \
  -d '{"name":"Pro","product_id":"'"$PRODUCT_ID"'","slug":"pro","tier":"pro","price_monthly":4900}'

Copy the returned plan idPLAN_ID. (price_monthly is in minor units, e.g. cents.)

4. Issue a license

The server generates the key — you do not supply one. The plaintext key is returned exactly once (it is stored hashed); copy it now.

curl -sX POST http://localhost:8000/api/v1/licenses \
  -H "Authorization: ApiKey test-operator-key" \
  -H "Content-Type: application/json" \
  -d '{"product_id":"'"$PRODUCT_ID"'","plan_id":"'"$PLAN_ID"'","max_activations":5,"duration_days":365}'
// response (illustrative)
{
  "id": "…",
  "key": "saw_default-tenant_my-app_live_a1b2c3d4e5f6a7b8c9d0_f0e1",  // shown once — copy it
  "key_hash": "…",                                           // only the hash is stored
  "status": "active"
}

The live / test segment in the key reflects the server's environment.

5. Validate it from an SDK

Pick your language — every SDK exposes the same validate() surface and handles the geometric-proof challenge internally. Python:

pip install sawabona-sdk
from sawabona_sdk import LicenseClient

with LicenseClient(
    "http://localhost:8000",
    "saw_default-tenant_my-app_live_a1b2c3d4e5f6a7b8c9d0_f0e1",  # the key from step 4
    "my-app",                                            # product slug
) as client:
    result = client.validate()
    print(result.is_valid())     # True
    print(result.features)       # plan features
    print(client.has_feature("pro"))

The same flow exists for Rust, TypeScript, Go, Java, C#, C, C++ and Ada — see the SDK index.

Validating directly over HTTP (POST /api/v1/licenses/validate) is possible but requires building and submitting a geometric proof yourself; the SDKs are the supported path.

Retiring things (there is no hard delete — by design)

Products, plans and licenses are never hard-deleted over the API: licenses reference products/plans by foreign key and audit/billing lineage must survive. Use the soft lifecycle instead:

  • LicensePOST /api/v1/licenses/{id}/revoke
  • PlanPATCH /api/v1/plans/{id} with {"is_active": false}
  • ProductPATCH /api/v1/products/{id} with {"is_active": false}

New licenses cannot be issued against an inactive product or plan; existing licenses are unaffected. Reactivate by patching is_active back to true.

Next steps