Skip to content
UsageRail
Reference / Server API

Server API

The server API lets your backend manage customers and keys directly: create a customer when someone signs up on your site, give them a key, read their usage, move them to a free plan, or sign them in to the portal. It's for calls from your server, never from a browser.

bash
curl https://api.usagerail.com/v1/consumers \
  -H "Authorization: Bearer $USAGERAIL_MANAGEMENT_KEY" \
  -H "Idempotency-Key: signup-8f14e45f" \
  -H "Content-Type: application/json" \
  -d '{"external_id": "user_1042", "email": "ada@example.com"}'

Authentication

Every call needs the app's server API key, which starts with urm_. Create it on the app's page in the console, under Server API key. It's separate from the app key the SDK uses, and like the app key it's shown once. After a rotation, the old one keeps working for 24 hours.

Send it as a bearer token. A wrong key gets a 401 invalid_management_key, and an address that keeps sending wrong keys gets a 429 for a while.

Idempotency

Every POST and DELETE needs an Idempotency-Key header of up to 255 characters, such as an ID from your own database. UsageRail keeps each key for 24 hours.

  • Sending the same request again with the same key returns the first response, with an Idempotent-Replayed: true header, and does nothing new.
  • The same key with a different method, path or body gets a 422 idempotency_key_reused.
  • A request that failed with a 500 leaves nothing behind, so you can retry it with the same key.
  • A new API key and a portal link appear only in the first response. A replay returns the same IDs and expiry time without them; use a new idempotency key to get a new one.

Customers

Every endpoint that returns a customer returns this object:

json
{
  "consumer": {
    "id": "cons_7Qm2xR9s",
    "email": "ada@example.com",
    "external_id": "4f9c1d0e7b6a...",
    "status": "active",
    "email_verified": false,
    "created_at": "2026-10-21T09:14:03.120Z",
    "plan": { "id": "plan_free", "name": "Free", "version": 1 },
    "quota": { "behavior": "stop", "limit": "1000" },
    "rate_limits": { "per_second": 2, "per_minute": null },
    "current_period": {
      "period_start": "2026-10-01T00:00:00.000Z",
      "period_end": "2026-11-01T00:00:00.000Z",
      "billable_requests": "37",
      "billable_units": "37"
    },
    "balance": null,
    "active_keys": 1
  }
}

Counts and limits are strings, so large numbers survive any JSON parser. external_id is stored hashed when your app hashes IDs, which is the default, so you get the hash back rather than the ID you sent. plan is null for a customer without a plan, and balance is only set on prepaid plans.

Create a customer

POST /v1/consumers with an optional external_id (your own ID for the customer, up to 256 characters) and an optional email.

The new customer starts on the app's default plan and the response is a 201. With an external_id that already belongs to a customer, you get that customer back with a 200 and nothing changes; use claim to add an email to an existing customer.

StatuserrorWhy
400invalid_external_id, invalid_email, invalid_jsonThe body isn't valid.
403new_consumer_blockedThe app isn't taking new customers right now.
409consumer_existsThe email belongs to another customer.
409consumer_deletedThe external_id belongs to a deleted customer.

Get a customer

GET /v1/consumers/{id} returns the customer, or a 404 not_found if there's no such customer in this app.

Claim an email or external ID

POST /v1/consumers/{id}/claim with exactly one of email or external_id attaches it to an existing customer. Use it to connect a customer who signed up in the portal to your own user ID, or to give a customer an email so they can sign in to the portal.

StatuserrorWhy
200Attached, or the customer already had that exact value.
400claim_one_identifierSend exactly one of the two fields.
409already_claimedThe customer already has a different value for that field.
409consumer_existsAnother customer has that value.
409consumer_pending_deletionThe customer is being deleted.

Keys

Create a key

POST /v1/consumers/{id}/keys with an optional name of up to 100 characters. The response is a 201:

json
{
  "key_id": "PRjuGinc",
  "consumer_id": "cons_7Qm2xR9s",
  "name": "Production",
  "key": "acme_live_PRjuGincAHq68hICyifjdBmNASZ1Fw9mwPOgy8pt0EJiLE"
}

key appears in this response only. Errors: 404 not_found, 409 consumer_not_active for a suspended customer, 409 key_limit_reached when the customer or the app has no room for another key, and 409 app_read_only while the app isn't taking new keys.

Revoke a key

DELETE /v1/keys/{key_id} revokes the key at once; running SDKs stop accepting it within about 20 seconds. The response is {"key_id": "PRjuGinc", "status": "revoked"}, or a 404 if there's no active key with that ID.

Usage

GET /v1/consumers/{id}/usage?days=7 returns the customer's usage for the last 1 to 30 days, 7 by default, counted in calendar days in the app's time zone:

json
{
  "days": 7,
  "retention_days": 30,
  "timezone": "America/New_York",
  "unit_mode": "requests",
  "daily": [
    { "day": "2026-10-21", "requests": "41", "billable_requests": "37", "billable_units": "37" }
  ],
  "endpoints": [
    { "method": "GET", "route": "/v1/items/:id", "requests": "41", "billable_requests": "37", "billable_units": "37" }
  ]
}

daily has one entry per day, including days without usage. endpoints lists the 20 busiest routes. requests counts every request, including refused ones; the billable fields count only the ones that are billed. If your plan keeps fewer days than you asked for, days says how many you got.

Plans

POST /v1/consumers/{id}/plan with a plan_id moves the customer to that plan straight away. Running SDKs pick up the new quota and rate limits within about 20 seconds.

For now only free plans work here. A plan with a price returns a 409 paid_plan_needs_checkout, because paid plans need billing through Stripe, which isn't live yet. Other errors: 404 plan_not_found or not_found, and 409 consumer_not_active.

POST /v1/consumers/{id}/portal-link returns a link that signs the customer in to your portal, with an optional next path to open after sign-in, such as /keys:

json
{
  "url": "https://acme.usagerail.app/auth/callback?token=...",
  "expires_at": "2026-10-21T09:29:03.120Z"
}

The link works once and for 15 minutes. It opens a page with a Sign in button, so a link previewed by an email scanner isn't used up.

StatuserrorWhy
409consumer_has_no_emailThe portal signs people in by email. Claim an email first.
409consumer_not_activeThe customer is suspended.
409portal_unavailableThe portal isn't available right now.

Errors

Errors are JSON with an error field. Besides the ones listed with each endpoint, any call can get a 401 invalid_management_key, a 404 not_found for a customer or key that isn't in this app, and a 410 app_deleted once the app is being deleted.