EcoQuota v1

EcoQuota API Reference

Auth, API keys, and quota enforcement, as a service. Put EcoQuota in front of your backend, hand each customer their own workspace, and let us count requests and enforce limits. This page documents everything you need: the verify endpoint, workspace management APIs, webhooks, and billing.

Verify endpoint: /v1/verify

One call your backend makes before serving a request. Returns 200 / 401 / 429 plus quota headers.

Workspace APIs: /api/v1

REST APIs for the dashboard: auth, keys, users, usage, and events. Authenticated with your session token.

Two different credentials: API keys (eq_live_…) are verified by /v1/verify and handed to your customers. Session tokens are used by you to manage the workspace via /api/*. Keep them separate.

#Quickstart

From zero to verifying keys in about two minutes.

curl
1. # Create a workspace (or do this in the Dashboard UI)
POST https://ecoquota.kodenesiadigital.workers.dev/api/auth/signup
{ "name": "Acme", "email": "team@acme.dev", "password": "s3cure-pass" }

2. # Create an API key for one of your customers
POST https://ecoquota.kodenesiadigital.workers.dev/api/v1/keys
Authorization: Bearer <your-session-token>
{ "user_email": "customer@client.com", "quota_limit": 5000 }

3. # In your backend, verify before serving the request
GET https://ecoquota.kodenesiadigital.workers.dev/v1/verify
Authorization: Bearer eq_live_xxxxxxxxxxxxxxxx

Response: 200 + quota object and X-Quota-* headers → allow the request. 401 → reject with the error code. 429 → quota exhausted, do not serve.

The key is returned only once at creation. Store it immediately — hashes are stored on our side, so plaintext keys can never be recovered.

#Authentication

EcoQuota uses two different credential types depending on the endpoint.

CredentialUsed forFormatHeader
API key/v1/verifyeq_live_ + 32 hex chars, shown onceAuthorization: Bearer <key>
Session tokenAll /api/* workspace endpointsreturned by signup / loginAuthorization: Bearer <token>

Create a workspace

POST/api/auth/signup
BodyTypeRequiredNotes
namestringyesmin 2 characters
emailemailyesunique — used to match Polar checkout
passwordstringyesmin 8 characters
curl
POST /api/auth/signup
{ "name": "Acme", "email": "team@acme.dev", "password": "s3cure-pass" }

# 201 Created
{
  "workspace": { "id": "…", "name": "Acme", "email": "team@acme.dev", "tier": "free", "active": 1 },
  "token": "ey…",                 // session token — store it
  "expires_at": "…"
}

Login

POST/api/auth/login
curl
POST /api/auth/login
{ "email": "team@acme.dev", "password": "s3cure-pass" }

# 200 OK — same shape as signup
{ "workspace": { … }, "token": "ey…", "expires_at": "…" }

Other auth endpoints

All require a session token.

POST/api/auth/logoutBearer session

Revokes the current session token. Body: none.

GET/api/auth/meBearer session

Returns the current workspace: { workspace: { id, name, email, tier, active, created_at } }.

PATCH/api/auth/passwordBearer session
BodyNotes
current_passwordmust match
new_passwordmin 8 characters
Tip: pass a session token the same way everywhere — Authorization: Bearer …. Invalid or expired sessions return 401 unauthorized.

#Verify endpoint

The core of EcoQuota. Call this from your backend before serving a request. It authenticates the API key and atomically increments the quota counter, then tells you what to do.

GET/v1/verifyalso POSTpublic
HeaderRequiredValue
AuthorizationyesBearer eq_live_…

Status codes you act on:

CodeMeaningAction
200Valid key, within quotaServe the request
401Missing / invalid / revoked key, or suspended workspaceReject with error code
429Quota exceededReject — do not serve

200 OK

response
{
  "ok": true,
  "key_id": "…",
  "key_prefix": "eq_live_abcd",
  "quota": {
    "period": "2026-09",         // YYYY-MM (monthly) or YYYY-MM-DD (daily), UTC
    "period_type": "monthly",
    "used": 321,
    "limit": 5000,
    "percent": 6,
    "remaining": 4679
  }
}

Every response also includes quota headers — useful for caching and client-side displays:

HeaderExample
X-Quota-Period2026-09
X-Quota-Period-Typemonthly | daily
X-Quota-Limit5000
X-Quota-Used321
X-Quota-Remaining4679
X-Quota-Percent6

401 Unauthorized

Body: { "ok": false, "error": "<code>" } — codes below.

CodeCause
missing_authorizationNo Authorization header
invalid_keyKey not found (wrong or revoked-for-another-reason token)
key_revokedKey exists but status is revoked
workspace_suspendedOwning workspace is disabled

429 Quota exceeded

response
// 429 Too Many Requests
{
  "ok": false,
  "error": "quota_exceeded",
  "quota": { "period": "2026-09", "period_type": "monthly", "used": 5001, "limit": 5000, "percent": 100, "remaining": 0 }
}
No silent overage: the counter is incremented atomically before the check, so a request at used = 5001 with limit = 5000 is always 429 — never accidental over-service. That request is also recorded in the denial counter.
Webhook triggers: at ≥80% usage a quota_80 event fires; at >100% a quota_100 event fires (once per key per period, if the user has a webhook URL configured).

#API keys

Keys live inside a workspace and are bound to a user (a workspace member representing one of your customers). All key endpoints require a session token.

Free tier: max 3 active keys and max quota limit of 5,000. Pro removes both.

Create a key

POST/api/v1/keysBearer session
BodyRequiredNotes
user_id or user_emailyesEither an existing user id, or an email — creates the user automatically when missing
namenodefaults to "default"
quota_periodnomonthly (default) | daily
quota_limitnointeger ≥ 1, default 5000
curl
POST /api/v1/keys
Authorization: Bearer <session-token>
{ "user_email": "customer@client.com", "quota_limit": 1000, "quota_period": "monthly" }

# 201 Created
{
  "id": "…", "user_id": "…", "name": "default",
  "key": "eq_live_1f3c9d2a…",      // shown ONLY here
  "key_prefix": "eq_live_1f3c",
  "status": "active",
  "quota_period": "monthly",
  "quota_limit": 1000,
  "note": "Save this key now — it is shown only once."
}

List keys

GET/api/v1/keysBearer session

Returns { "keys": [ … ] } ordered newest-first, with a usage object per key: { period, used, limit, percent }. Secure fields (key_hash) are stripped.

Get a key

GET/api/v1/keys/:idBearer session

Same shape as one list item, quoted with usage. 404 when the key is not in your workspace.

Update a key

PATCH/api/v1/keys/:idBearer session
BodyValues
nameany string
statusactive | revoked (instant kill switch)
quota_perioddaily | monthly
quota_limitinteger ≥ 1; free tier capped at 5,000

Delete a key

DELETE/api/v1/keys/:idBearer session

Regenerate a key

POST/api/v1/keys/:id/regenerateBearer session

Issues a brand-new secret for the same key id. The old secret is deactivated immediately. Returns { id, key, key_prefix, note } — same one-time-warning applies.

#Users

A user is a member of your workspace — practically one of your customers. A user owns API keys and can receive webhook notifications. All endpoints require a session token.

List users

GET/api/v1/usersBearer session

Each row includes active_keys and total_usage aggregates.

Create a user

POST/api/v1/usersBearer session
BodyRequiredNotes
emailyesunique per workspace, lower-cased
namenodefaults to the email
tiernofree (default) | pro
webhook_urlnowhere quota events are POSTed
curl
POST /api/v1/users
Authorization: Bearer <session-token>
{ "name": "DataCorp", "email": "ops@datacorp.io", "webhook_url": "https://api.datacorp.io/ecoquota/hook" }

# 201 Created — the secret is returned only here (used to verify webhook signatures)
{ "user": { … }, "webhook_secret": "2f6a…" }

Get / update / delete a user

GET/api/v1/users/:idBearer session
PATCH/api/v1/users/:idBearer session

Fields: name, tier (free | pro), webhook_url, webhook_secret.

DELETE/api/v1/users/:idBearer session

#Webhooks

EcoQuota notifies your backend about quota milestones so you can react automatically (extend a trial, hard-block at 100%, upsell). Events are delivered per user (all their keys) to the webhook_url you set.

EventTriggerFrequency
quota_80Usage reaches ≥80% of the limitonce per key per period
quota_100Usage exceeds 100% (request that got 429)once per key per period
test“Send Test Webhook” in the dashboardon demand

Delivery

webhook
POST <your webhook_url>
Content-Type: application/json
User-Agent: EcoQuota/1.0
X-EcoQuota-Event: quota_80
X-EcoQuota-Signature: sha256=<lowercase-hex-hmac>

{
  "event": "quota_80",
  "key_id": "…",
  "key_prefix": "eq_live_1f3c",
  "key_name": "default",
  "user": { "id": "…", "name": "DataCorp", "email": "ops@datacorp.io" },
  "quota": { "period": "2026-09", "period_type": "monthly", "used": 4000, "limit": 5000, "percent": 80, "remaining": 1000 },
  "timestamp": "2026-09-05T12:00:00.000Z"
}

Verify signatures

Signatures are HMAC-SHA256 of the raw request body, hex-encoded, using your user’s webhook_secret:

signature
signature = sha256 = HEX( HMAC-SHA256( secret, rawBody ) )   // lowercase hex
Node.js
// Node.js — validate before trusting the payload
const crypto = require("crypto");

function verify(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody, "utf8")
    .digest("hex");
  const provided = signatureHeader.replace(/^sha256=/, "");
  const a = Buffer.from(expected), b = Buffer.from(provided);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// const ok = verify(req.rawBody, req.headers["x-ecoquota-signature"], userSecret);
Respond 2xx quickly. Delivery is fire-and-forget today (retries come later), so your endpoint should ideally be idempotent.

#Quotas & usage

Each key has a quota_limit and a quota_period — either monthly (resets 1st of month UTC) or daily (resets midnight UTC).

ConceptDetails
Period keymonthly: YYYY-MM · daily: YYYY-MM-DD (UTC)
Countingincremented atomically on every /v1/verify call
Enforcementused > limit429 quota_exceeded
Denialstracked separately (denial_counters), shown as “Denied today”
Percentfloored integer, capped at 100

Dashboard usage

GET/api/v1/dashboard/summaryBearer session

Returns { summary: { users, active_keys, requests_today, requests_month, denied_today, period_today, period_month } }.

GET/api/v1/dashboard/usage?period=YYYY-MMBearer session

Per-key usage for a period, sorted by usage descending. Each row has percent computed for you.

GET/api/v1/dashboard/eventsBearer session

Recent webhook events (last 50), including delivery status.

#Billing & Pro

Upgrade your workspace to Pro from the landing page or dashboard. Payments are handled by Polar (Stripe-powered checkout) — EcoQuota never touches card data.

GET/api/billing/polar/checkoutpublic

Returns { "url": "https://buy.polar.sh/…" } — use it to build custom upgrade buttons.

FreePro — $5/month
Active API keys3Unlimited
Quota limit per keymax 5,000Unlimited
Workspaces11
Usage dashboard
Webhooks (quota_80 / quota_100)
Priority support
Important: check out with the same email as your EcoQuota account. Your tier is matched by that email and upgraded automatically as soon as order.paid, subscription.active, or benefit_grant.created arrives from Polar.

The POST /api/billing/polar/webhook endpoint is internal — Polar calls us with a Standard-Webhooks signature (webhook-id, webhook-timestamp, webhook-signature), verified against POLAR_WEBHOOK_SECRET before any tier change.

#Errors

API errors are always JSON: { "error": "<code>", "message": "human readable" } (except /v1/verify, which uses { "ok": false, "error": … }).

ErrorWhereHTTPMeaning
invalid_namesignup400name < 2 characters
invalid_emailsignup400not a valid email
invalid_passwordsignup / password400password < 8 characters
email_takensignup / users409email already registered
credentials_requiredlogin400missing email or password
invalid_credentialslogin401wrong email or password
workspace_suspendedlogin / verify403 / 401workspace disabled
unauthorizedany auth401missing / invalid / expired session
user_id_requiredcreate key400neither user_id nor user_email
tier_limitkeys403free-tier cap (3 keys / 5,000 limit)
user_not_foundtest webhook404unknown user_id
no_webhook_urltest webhook400user has no webhook_url
not_foundkeys / users404resource missing
no_fieldsPATCH keys400nothing to update
internal_errorany500unexpected failure

#Code examples

Confirm a key with curl

curl
curl -s -o /dev/null -w "%{http_code}\n" \
  https://ecoquota.kodenesiadigital.workers.dev/v1/verify \
  -H "Authorization: Bearer eq_live_xxxxxxxx"
# → 200, 401, or 429

Node.js

JavaScript
async function checkKey(key) {
  const res = await fetch("https://ecoquota.kodenesiadigital.workers.dev/v1/verify", {
    headers: { "Authorization": `Bearer ${key}` },
  });
  const body = await res.json();
  if (res.status === 200) {
    return { allow: true, quota: body.quota };
  }
  return { allow: false, reason: body.error || body.message, quota: body.quota };
}

Python

Python
import requests

def check_key(api_key):
    r = requests.get(
        "https://ecoquota.kodenesiadigital.workers.dev/v1/verify",
        headers={"Authorization": f"Bearer {api_key}"},
    )
    body = r.json()
    return r.ok, body.get("quota"), body.get("error")

Go

Go
import (
  "encoding/json"
  "net/http"
)

func verifyKey(key string) (int, string) {
  req, _ := http.NewRequest(http.MethodGet, "https://ecoquota.kodenesiadigital.workers.dev/v1/verify", nil)
  req.Header.Set("Authorization", "Bearer "+key)
  res, err := http.DefaultClient.Do(req)
  if err != nil { return 500, "" }
  defer res.Body.Close()
  var body struct { Error string `json:"error"` }
  json.NewDecoder(res.Body).Decode(&body)
  return res.StatusCode, body.Error
}

Upgrade a workspace (client-side)

JavaScript
const { url } = await fetch("https://ecoquota.kodenesiadigital.workers.dev/api/billing/polar/checkout").then(r => r.json());
window.location.href = url; // → https://buy.polar.sh/…

#FAQ

What is an API key vs a session token? Keys (eq_live_…) authenticate requests to your API via /v1/verify. Session tokens authenticate you to the workspace management API (/api/*).

How do customers get keys? You create them per user in the dashboard or via POST /api/v1/keys — customers never touch your management API.

Where can I see usage? Overview cards (today / month / denied), the per-key usage table, and via GET /api/v1/dashboard/*.

Do limits reset automatically? Yes — daily at 00:00 UTC, monthly on the 1st. Counters are keyed by the UTC period.

What happens when a key is revoked? The very next /v1/verify returns 401 key_revoked.

Can customers see the raw key again? No. Raw keys are shown once; only hashes are stored. Use POST /:id/regenerate to rotate.

Which email for Polar checkout? Same one as your EcoQuota account — the tier upgrade is matched by email.

Do you support other periods? Today monthly and daily. Anticipated: custom windows, tokens with expiry, and key metadata.

Rate limits on verify? No rate limit today; this API is designed to be called from your backend. Page/global limits are on the roadmap.