Appearance
Any language (HTTP)
The FastAPI SDK is a convenience layer over three HTTP endpoints, the SDK API. From Node, Go, Rails, Laravel or anything else, you can do the same job with a few calls.
Authentication
Every call sends your API's SDK secret key, from the Connect tab:
http
Authorization: Bearer mas_...The base URL is https://api.microauth.com. Errors use application/problem+json with a detail you can show in logs. A rotated or wrong secret gets 401, and an API suspended by MicroAuth gets 403.
Keys are matched by hash
Your customers send their key (map_...) to your API. You never forward it to MicroAuth. You send its SHA-256 hex digest instead: 64 lowercase hex characters. The snapshot lists key hashes and the verify endpoint takes a key_hash.
The simple version
Two calls cover it. Before your handler runs, look up the key. After the response, report it.
bash
# 1. Check the key a request came with. Send its SHA-256, never the key.
HASH=$(printf %s "$CUSTOMER_KEY" | shasum -a 256 | cut -d' ' -f1)
curl -s "https://api.microauth.com/sdk/v1/keys/verify?key_hash=$HASH" \
-H "Authorization: Bearer $MICROAUTH_SECRET_KEY"
# 2. Report the request when it's done. Retrying with the same
# idempotency_key never bills twice.
curl -s -X POST "https://api.microauth.com/sdk/v1/usage" \
-H "Authorization: Bearer $MICROAUTH_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"items":[{"idempotency_key":"req_8f2c1a9d","api_key_id":"<key_id>",
"usage_policy_id":"<usage_policy_id>","status_code":200,"count":1,
"period_start":"2026-10-11T12:00:00Z"}]}'Cache each lookup for up to 30 seconds and send usage in batches, as in the Node example below. For busy APIs, load the whole snapshot instead, so keys are checked in memory with no call at all.
The pattern at scale
text
your API process MicroAuth
+------------------------+
| keys and customers | every 30s GET /sdk/v1/snapshot
| in memory <-----------+-----------------------------------
| | new key GET /sdk/v1/keys/verify
| per request: hash the +----------------------------------->
| key, look it up, check |
| limits, count it | every 5s POST /sdk/v1/usage
| usage counters --------+----------------------------------->
+------------------------+A known key never needs a network call. The verify endpoint covers keys created after your latest snapshot, and usage goes out in batches.
GET /sdk/v1/snapshot
Everything you need to check keys locally: every customer with their effective limits, and every active key hash.
bash
curl https://api.microauth.com/sdk/v1/snapshot \
-H "Authorization: Bearer mas_..."json
{
"tenant_id": "0199d4f2-6c1e-7a3b-9f10-2b6c8d4e5a71",
"tenant_name": "Weather API",
"generated_at": "2026-10-11T12:00:00Z",
"billable_status_codes": [200, 201, 202, 203, 204, 205, 206],
"platform_monthly_limit": 1000000,
"platform_monthly_used": 48210,
"platform_monthly_remaining": 951790,
"platform_period_end": "2026-11-01T00:00:00Z",
"platform_hard_cap": true,
"customers": [
{
"id": "0199d4f2-7a10-7c55-8e2b-41f0d9a3c6e8",
"status": "active",
"credit_balance_micro": 12500000,
"balance_seq": 1842,
"month_requests": 48210,
"usage_policy_id": "0199d4f3-0b6e-7f21-a4c9-6d2e8b1f0a37",
"policy_valid_until": "2026-10-12T12:00:00Z",
"effective": {
"rps": 10,
"price_per_request_micro": 1000,
"monthly_quota": 100000,
"source": "plan",
"billing_model": "subscription"
}
}
],
"keys": [
{
"id": "0199d4f2-9c3d-7e08-b5a1-27c4e6f8d901",
"key_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"customer_id": "0199d4f2-7a10-7c55-8e2b-41f0d9a3c6e8"
}
]
}tenant_idandtenant_nameare your API's ID and name.statusisactive,pending(waiting for your approval) orsuspended. Onlyactivecustomers may call your API.effectiveis the result of limit resolution.monthly_quotais left out when there is no cap, andrpsof0means no limit.balance_seqversions the balance. It only goes up, so when two values disagree, the higherbalance_seqis the newer balance.- Keep each customer's
usage_policy_idwith the cached limits and send it with the usage of requests you admitted under it. Those requests are then charged at the prices that applied when you served them.policy_valid_untilis the latest traffic time the policy covers, not a deadline for reporting. platform_monthly_*is your MicroAuth plan's allowance for the calendar month in UTC, not one customer's quota. Refuse requests with429onceplatform_monthly_remainingreaches zero.- Refresh about every 30 seconds. The limit is 120 requests a minute per API.
- Send the last
ETagback asIf-None-Match. When nothing changed, MicroAuth answers304 Not Modifiedwith no body and anX-MicroAuth-Generated-Atheader. Keep your cached data and treat it as fresh as of that time.
GET /sdk/v1/keys/verify
Looks up one key hash, for keys that aren't in your snapshot yet because they were created seconds ago.
bash
curl "https://api.microauth.com/sdk/v1/keys/verify?key_hash=e3b0c442..." \
-H "Authorization: Bearer mas_..."json
{
"valid": true,
"key_id": "0199d4f2-9c3d-7e08-b5a1-27c4e6f8d901",
"billable_status_codes": [200, 201, 202, 203, 204, 205, 206],
"customer": {
"id": "0199d4f2-7a10-7c55-8e2b-41f0d9a3c6e8",
"status": "active",
"credit_balance_micro": 12500000,
"balance_seq": 1842,
"month_requests": 48210,
"usage_policy_id": "0199d4f3-0b6e-7f21-a4c9-6d2e8b1f0a37",
"policy_valid_until": "2026-10-12T12:00:00Z",
"effective": { "rps": 10, "price_per_request_micro": 1000, "monthly_quota": 100000, "source": "plan", "billing_model": "subscription" }
}
}An unknown or revoked key returns {"valid": false} with status 200: the call worked, the key didn't.
- Only call it when a key isn't in your cache, and let concurrent requests for the same hash share one call.
- Remember invalid hashes for about 30 seconds, so a flood of made up keys is absorbed by your cache.
- The limit is 600 requests a minute per API.
POST /sdk/v1/usage
Reports served requests in batches. For each item, MicroAuth records the usage, updates the customer's monthly count and charges their balance when the status code is billable.
bash
curl -X POST https://api.microauth.com/sdk/v1/usage \
-H "Authorization: Bearer mas_..." \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"idempotency_key": "f3cf32d1-e2b8-4d33-a37e-60f5f70b92e8",
"api_key_id": "0199d4f2-9c3d-7e08-b5a1-27c4e6f8d901",
"usage_policy_id": "0199d4f3-0b6e-7f21-a4c9-6d2e8b1f0a37",
"status_code": 200,
"count": 42,
"period_start": "2026-10-11T12:00:00Z"
}
]
}'json
{
"accepted": 1,
"results": [
{
"idempotency_key": "f3cf32d1-e2b8-4d33-a37e-60f5f70b92e8",
"status": "accepted",
"customer_id": "0199d4f2-7a10-7c55-8e2b-41f0d9a3c6e8",
"charged_micro": 42000,
"recorded_at": "2026-10-11T12:34:56.789Z"
}
],
"customers": [
{ "id": "0199d4f2-7a10-7c55-8e2b-41f0d9a3c6e8", "credit_balance_micro": 12458000, "balance_seq": 1843 }
]
}| Field | Rules |
|---|---|
idempotency_key | Required. 8 to 128 letters, digits, dots, underscores, colons or dashes. Create it once, before the first send |
api_key_id | Required. The key's id from the snapshot or verify |
status_code | Required. The HTTP status your API answered with |
count | Required. 1 to 10,000,000 requests |
period_start | Required. RFC 3339 with an offset, such as 2026-10-11T12:00:00Z. Bucketed to the hour. At most 45 days old and 5 minutes in the future |
usage_policy_id | Recommended. The customer's policy from the snapshot. Without it, current prices apply |
Each result has a status:
acceptedmeans the item was applied now.duplicatemeans an identical item with that idempotency key was applied earlier. Both are final: remove the item from your queue.rejectedcomes withhttp_statusand adetail, for example a key from another API. Don't retry it unchanged. Reusing an idempotency key with different content is rejected with409.retryis temporary. Keep the item and send it again unchanged.
Accepted and duplicate results also carry the receipt MicroAuth keeps for the idempotency key:
| Field | Meaning |
|---|---|
customer_id | The customer the item was recorded for |
charged_micro | What the item charged, in micro-USD. 0 when the status isn't billable. A duplicate returns the original charge |
recorded_at | When MicroAuth first recorded the item. A duplicate returns the original time |
Keep receipts if you reconcile your own records with MicroAuth's ledger. An item with a billable status is charged its count times the price of the policy it was sent with, so you can check every receipt against what you expected to charge.
customers holds the balance of every customer in the batch after it was charged. When its balance_seq is higher than the one you hold, replace your cached balance and drop the local holds for the items just acknowledged.
Report every authenticated response, billable or not, with its real status. Quotas and your plan's allowance count all of them, and only billable statuses cost money. A batch holds 1 to 1000 items, and the limit is 600 requests a minute per API. If a call fails or times out, keep the items and retry with backoff: the idempotency key makes the retry safe.
Checking a request
For each incoming request:
- Read the key from your header,
X-API-Keyby default. No key means401. - Hash it and look it up in your snapshot. If it's missing, call verify. Still unknown means
401. - A customer whose
statusisn'tactivegets403. - If
billing_modelisn'tnoneand the price is above zero, a balance below the price gets402. Subtract what you counted since the snapshot. - If there's a
monthly_quotaandmonth_requestsplus your local count reaches it, answer429. - If
platform_monthly_remainingis used up, answer429. - Apply
effective.rpsper customer. Over the limit means429withRetry-After. With more than one process, keep the counter in Redis. - Serve the request, then count it by key, policy, status code and hour.
Node example
An Express middleware that checks keys with a 30 second cache, enforces suspension, balance and quota, and reports usage every 5 seconds. It keeps its state in memory, so it suits one process. For several processes, or strict per second limits, use the snapshot and Redis as described above.
js
import { createHash, randomUUID } from "node:crypto";
const BASE = "https://api.microauth.com/sdk/v1";
const AUTH = { Authorization: `Bearer ${process.env.MICROAUTH_SECRET_KEY}` };
const cache = new Map();
const pending = [];
async function lookup(hash) {
const hit = cache.get(hash);
if (hit && Date.now() - hit.at < 30_000) return hit.result;
const res = await fetch(`${BASE}/keys/verify?key_hash=${hash}`, { headers: AUTH });
if (!res.ok) throw new Error(`MicroAuth answered ${res.status}`);
const result = await res.json();
cache.set(hash, { at: Date.now(), result });
return result;
}
export function microauth(header = "x-api-key") {
return async (req, res, next) => {
const key = req.get(header);
if (!key) return res.status(401).json({ error: "Missing API key" });
const hash = createHash("sha256").update(key).digest("hex");
let v;
try {
v = await lookup(hash);
} catch {
return res.status(503).json({ error: "Try again shortly" });
}
if (!v.valid) return res.status(401).json({ error: "Invalid API key" });
const c = v.customer;
const e = c.effective;
if (c.status !== "active") return res.status(403).json({ error: "This account is suspended" });
const price = e.billing_model === "none" ? 0 : e.price_per_request_micro;
if (price > 0 && c.credit_balance_micro < price) return res.status(402).json({ error: "Add credit to continue" });
if (e.monthly_quota && c.month_requests >= e.monthly_quota) return res.status(429).json({ error: "Monthly quota reached" });
req.customer = c;
res.on("finish", () => {
pending.push({
idempotency_key: randomUUID(),
api_key_id: v.key_id,
usage_policy_id: c.usage_policy_id,
status_code: res.statusCode,
count: 1,
period_start: new Date().toISOString(),
});
});
next();
};
}
setInterval(async () => {
const items = pending.splice(0, 1000);
if (!items.length) return;
try {
const res = await fetch(`${BASE}/usage`, {
method: "POST",
headers: { ...AUTH, "Content-Type": "application/json" },
body: JSON.stringify({ items }),
});
const body = res.ok ? await res.json() : { results: [] };
const done = new Set(body.results.filter((r) => r.status !== "retry").map((r) => r.idempotency_key));
pending.unshift(...items.filter((i) => !done.has(i.idempotency_key)));
} catch {
pending.unshift(...items);
}
}, 5_000).unref();js
import express from "express";
import { microauth } from "./microauth.js";
const app = express();
app.get("/forecast", microauth(), (req, res) => res.json({ customer: req.customer.id }));
app.listen(8000);Before you rely on it in production, bound the pending queue, keep it on disk so a restart doesn't lose usage, and drain it on shutdown.
See also
- The API reference has every field of these endpoints.
- Troubleshooting helps when the dashboard never shows your API as connected.
- The platform API manages customers, credit and plans from your own systems.