# Varial Energy API - Developer Guide

> Quickstart and conventions for the developer API (/api/v1): mint a token, clone a template scenario, submit a run, poll, and download results, no clicking required.

Clone scenarios, submit runs, and pull results from your own pipeline, no clicking required. The full OpenAPI reference lives at </api/v1/docs/>; tokens are managed under [Settings → API](/settings/api/).

## Getting access

The developer API is currently available as a pilot for selected teams. If your team is enrolled in the pilot, you can mint API tokens yourself under [Settings → API](/settings/api/) and follow the quickstart below.

If your team isn't enrolled yet, [talk to us about access](/contact/?message=Hi%2C%20I%27d%20like%20access%20to%20the%20Varial%20developer%20API.) and we'll get you set up. The API lets you drive Varial from your own pipeline — clone a template scenario, tweak its inputs, submit a run, poll for completion, and download forecast results, all over a scoped, token-authenticated REST API.

## Quickstart

The core loop: verify your token, clone a template scenario you've configured in the web UI, tweak it, submit a run, poll until it completes, download the results.

**1. Mint a token**

Go to [Settings → API](/settings/api/), name the token, pick its access level and expiry, and copy the `vrl_…` string; it is shown once. Then verify it:

```bash
curl -H "Authorization: Bearer $VARIAL_TOKEN" \
  https://varialenergy.com/api/v1/me/
```

> **INFO:** Using Python's stdlib urllib?
>
> Requests made with Python's raw `urllib.request.urlopen` (the default `Python-urllib/x.y` User-Agent) get an HTTP 403 from the CDN in front of `/api/v1/`, before the request ever reaches Django auth. It's a bot-signature block, not an API error. Set an explicit `User-Agent` header to any non-default value and it goes away; `requests`, `httpx`, and curl are unaffected.

**2. Clone a template scenario**

Configure a scenario once in the web UI (demand, supply, network, everything), then clone it as many times as you need. The clone carries every input; the body is a patch applied on top:

```bash
curl -X POST -H "Authorization: Bearer $VARIAL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "clone_from": "<template-scenario-id>",
    "name": "High gas sensitivity 001",
    "scenarios": [{"name": "Central", "weather_years": [2015, 2016]}]
  }' \
  https://varialenergy.com/api/v1/scenarios/
```

The response is the scenario's full config document plus a `validation` block telling you whether it is ready to submit. Adjustor cells can be set declaratively via `PATCH /api/v1/scenarios/{id}/config/`.

**3. Preview the cost, then submit**

```bash
curl -X POST -H "Authorization: Bearer $VARIAL_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: run-2026-07-11-001" \
  -d '{"scenario_id": "<scenario-id>"}' \
  https://varialenergy.com/api/v1/runs/
```

Runs debit credits, so the `Idempotency-Key` header is required; retrying with the same key returns the original run instead of charging twice.

**4. Poll, then download**

```bash
curl -H "Authorization: Bearer $VARIAL_TOKEN" \
  https://varialenergy.com/api/v1/runs/<run-id>/

curl -H "Authorization: Bearer $VARIAL_TOKEN" -o prices.csv \
  "https://varialenergy.com/api/v1/runs/<run-id>/results/?view=prices&output=csv"
```

Non-terminal runs return a `Retry-After` header with the recommended poll interval. Results return `409 run_not_completed` until the run reaches `COMPLETED`.

## Authentication &amp; scopes

Every request carries `Authorization: Bearer vrl_…`. Tokens expire (you'll be emailed at 14 and 3 days out) and can be revoked at any time by you or your team's admins. Tokens only work on `/api/v1/`; they never grant access to the web application.

A token holds a set of scopes, chosen at minting time:

| Scope | Grants |
| --- | --- |
| `account:read` | Your user and team profile (`/me`) |
| `market-data:read` | Markets and regions |
| `scenarios:read` / `scenarios:write` | List/read vs create, clone, and patch projects and scenarios |
| `runs:read` / `runs:write` | List/poll vs submit runs (submitting spends credits) |
| `results:read` | Download run results |
| `credits:read` | Your team's credit balance |

## Conventions

**Errors.** Failures return `{"error": {"code", "detail"}}`. Machine-readable codes include `insufficient_credits` (402), `scenario_locked` and `run_not_completed` (409), `validation_failed`, `idempotency_key_required`, `read_only_field`, `unknown_field` (400), and `throttled` (429).

**Pagination.** List endpoints return `{"data": [...], "next_cursor", "previous_cursor"}`. Follow `next_cursor` until it is null; cursors are stable under concurrent inserts. Page size via `?page_size=` (max 200).

**Rate limits.** Per token: 60 requests/minute burst, 1,000/hour sustained. Per team: 5,000/hour across all tokens. 429 responses carry `Retry-After`.

**Run lifecycle.** Runs report a raw `status` (the exact state machine value) and a collapsed `phase`: `queued → running → completed`, with `waiting_capacity` (transient, poll slower), `failed`, and `cancelled`. Credits are refunded automatically when a run fails.

**Config documents.** A scenario's configuration is one JSON document (`schema_version: 1`). `PATCH` follows merge semantics: keys you omit are untouched; the `scenarios` list, when present, is the complete desired set (existing entries matched by id/name keep their adjustors unless you send an `adjustors` key, which replaces that set declaratively). Scenarios with an active or completed run are locked; clone instead.

## Versioning

The contract under `/api/v1/` only changes additively: new endpoints, new optional fields. Breaking changes would ship as `/api/v2/` with a long overlap. Every change is recorded in the [changelog](/developers/changelog/); the machine-readable spec lives at </api/v1/schema/>.