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.
One call your backend makes before serving a request. Returns 200 / 401 / 429 plus quota headers.
REST APIs for the dashboard: auth, keys, users, usage, and events. Authenticated with your session token.
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.
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_xxxxxxxxxxxxxxxxResponse: 200 + quota object and X-Quota-* headers → allow the request. 401 → reject with the error code. 429 → quota exhausted, do not serve.
#Authentication
EcoQuota uses two different credential types depending on the endpoint.
| Credential | Used for | Format | Header |
|---|---|---|---|
| API key | /v1/verify | eq_live_ + 32 hex chars, shown once | Authorization: Bearer <key> |
| Session token | All /api/* workspace endpoints | returned by signup / login | Authorization: Bearer <token> |
Create a workspace
| Body | Type | Required | Notes |
|---|---|---|---|
name | string | yes | min 2 characters |
email | yes | unique — used to match Polar checkout | |
password | string | yes | min 8 characters |
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
{ "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.
Revokes the current session token. Body: none.
Returns the current workspace: { workspace: { id, name, email, tier, active, created_at } }.
| Body | Notes |
|---|---|
current_password | must match |
new_password | min 8 characters |
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.
| Header | Required | Value |
|---|---|---|
Authorization | yes | Bearer eq_live_… |
Status codes you act on:
| Code | Meaning | Action |
|---|---|---|
| 200 | Valid key, within quota | Serve the request |
| 401 | Missing / invalid / revoked key, or suspended workspace | Reject with error code |
| 429 | Quota exceeded | Reject — do not serve |
200 OK
{
"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:
| Header | Example |
|---|---|
X-Quota-Period | 2026-09 |
X-Quota-Period-Type | monthly | daily |
X-Quota-Limit | 5000 |
X-Quota-Used | 321 |
X-Quota-Remaining | 4679 |
X-Quota-Percent | 6 |
401 Unauthorized
Body: { "ok": false, "error": "<code>" } — codes below.
| Code | Cause |
|---|---|
missing_authorization | No Authorization header |
invalid_key | Key not found (wrong or revoked-for-another-reason token) |
key_revoked | Key exists but status is revoked |
workspace_suspended | Owning workspace is disabled |
429 Quota exceeded
// 429 Too Many Requests
{
"ok": false,
"error": "quota_exceeded",
"quota": { "period": "2026-09", "period_type": "monthly", "used": 5001, "limit": 5000, "percent": 100, "remaining": 0 }
}used = 5001 with limit = 5000 is always 429 — never accidental over-service. That request is also recorded in the denial counter.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.
Create a key
| Body | Required | Notes |
|---|---|---|
user_id or user_email | yes | Either an existing user id, or an email — creates the user automatically when missing |
name | no | defaults to "default" |
quota_period | no | monthly (default) | daily |
quota_limit | no | integer ≥ 1, default 5000 |
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
Returns { "keys": [ … ] } ordered newest-first, with a usage object per key: { period, used, limit, percent }. Secure fields (key_hash) are stripped.
Get a key
Same shape as one list item, quoted with usage. 404 when the key is not in your workspace.
Update a key
| Body | Values |
|---|---|
name | any string |
status | active | revoked (instant kill switch) |
quota_period | daily | monthly |
quota_limit | integer ≥ 1; free tier capped at 5,000 |
Delete a key
Regenerate a key
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
Each row includes active_keys and total_usage aggregates.
Create a user
| Body | Required | Notes |
|---|---|---|
email | yes | unique per workspace, lower-cased |
name | no | defaults to the email |
tier | no | free (default) | pro |
webhook_url | no | where quota events are POSTed |
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
Fields: name, tier (free | pro), webhook_url, webhook_secret.
#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.
| Event | Trigger | Frequency |
|---|---|---|
quota_80 | Usage reaches ≥80% of the limit | once per key per period |
quota_100 | Usage exceeds 100% (request that got 429) | once per key per period |
test | “Send Test Webhook” in the dashboard | on demand |
Delivery
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 = sha256 = HEX( HMAC-SHA256( secret, rawBody ) ) // lowercase hex
// 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);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).
| Concept | Details |
|---|---|
| Period key | monthly: YYYY-MM · daily: YYYY-MM-DD (UTC) |
| Counting | incremented atomically on every /v1/verify call |
| Enforcement | used > limit → 429 quota_exceeded |
| Denials | tracked separately (denial_counters), shown as “Denied today” |
| Percent | floored integer, capped at 100 |
Dashboard usage
Returns { summary: { users, active_keys, requests_today, requests_month, denied_today, period_today, period_month } }.
Per-key usage for a period, sorted by usage descending. Each row has percent computed for you.
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.
Returns { "url": "https://buy.polar.sh/…" } — use it to build custom upgrade buttons.
| Free | Pro — $5/month | |
|---|---|---|
| Active API keys | 3 | Unlimited |
| Quota limit per key | max 5,000 | Unlimited |
| Workspaces | 1 | 1 |
| Usage dashboard | ✓ | ✓ |
| Webhooks (quota_80 / quota_100) | ✓ | ✓ |
| Priority support | — | ✓ |
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": … }).
| Error | Where | HTTP | Meaning |
|---|---|---|---|
invalid_name | signup | 400 | name < 2 characters |
invalid_email | signup | 400 | not a valid email |
invalid_password | signup / password | 400 | password < 8 characters |
email_taken | signup / users | 409 | email already registered |
credentials_required | login | 400 | missing email or password |
invalid_credentials | login | 401 | wrong email or password |
workspace_suspended | login / verify | 403 / 401 | workspace disabled |
unauthorized | any auth | 401 | missing / invalid / expired session |
user_id_required | create key | 400 | neither user_id nor user_email |
tier_limit | keys | 403 | free-tier cap (3 keys / 5,000 limit) |
user_not_found | test webhook | 404 | unknown user_id |
no_webhook_url | test webhook | 400 | user has no webhook_url |
not_found | keys / users | 404 | resource missing |
no_fields | PATCH keys | 400 | nothing to update |
internal_error | any | 500 | unexpected failure |
#Code examples
Confirm a key with 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 429Node.js
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
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
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)
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.