---
title: "SERVICE ONLY: issue a free read-only key for a Telegram user"
method: POST
path: "/v1/service/free-keys"
tags: ["Service"]
---

# SERVICE ONLY: issue a free read-only key for a Telegram user

`POST /v1/service/free-keys`

## Request

- Base URL: `https://intelapi.publicaml.org`
- URL: `https://intelapi.publicaml.org/v1/service/free-keys`
- Auth: API key in header `X-Signature` and API key in header `X-API-Key` and API key in header `X-Timestamp`

Not for integrators. Called by the PublicAML Telegram bot (@PublicAMLbot) to hand its users a free read-only API key.

Only an allowlisted key-issuing service may call it, with an HMAC-signed request. Everyone else gets `403 key_issuer_required`: anonymous callers, bare `X-API-Key` requests (including the service's own key sent unsigned) and signed requests from any other customer.

What is issued is fixed server-side: tier `free`, no HMAC secret (the key is sent bare as `X-API-Key` and can only read), the free-tier quota in units per `window_sec`, a `tg_` prefix, and `customer_id = tg-<telegram_user_id>`.

Idempotent per `external_id`: an active key is returned again with `reissued: true` and status 200. A revoked or expired key is `409 key_revoked` and no new key is minted. New keys are capped per rolling 24 hours across the whole endpoint (`429 daily_issue_limit`); returning an existing key is never capped.

`create: false` only looks: it returns the user's key (200) or `404 no_key`, never mints one, and neither spends nor is limited by the daily budget. It is what the bot's `/mykey` uses.

200 and 201 carry `usage`: the key's quota spend in the current rate-limit window (the same counter as its `RateLimit-*` headers), plus 24 h and 30 d totals, or `null` when that could not be read.

## Request body

- object
  - `external_id` string, required: `tg:<telegram_user_id>`. Decides the customer id (`tg-<id>`) one-to-one.
  - `display` string: Optional human-readable name. Written to the service log next to the key fingerprint; not stored with the key and never returned.
  - `create` boolean: false = look up only: return the existing key or `404 no_key`, never create one. Must be a JSON boolean.

## Response `200`

The user already holds an active key; it is returned again (`reissued: true`).

- IssuedFreeKey
  - `api_key` string, required: `tg_…`. Send it as `X-API-Key` on every call; it has no secret and cannot sign.
  - `key_hint` string: 16-hex fingerprint, safe to log. The same id usage accounting groups by.
  - `customer_id` string: `tg-<telegram_user_id>`.
  - `external_id` string
  - `tier` 'free' | 'pro' | 'enterprise', required
  - `hmac` false
  - `rate_limit_units` integer, nullable, required: Units (addresses) this key may spend per `window_sec`, metered per key across every endpoint. null = unmetered.
  - `window_sec` integer, required: Rate-limit window in seconds (3600 in production).
  - `created_at` string, date-time, required
  - `reissued` boolean, required: true when an existing active key was returned instead of a new one.
  - `docs` object
    - `base_url` string
    - `console` string
    - `openapi` string
    - `rate_limits` string
  - `usage` object, nullable: How much of its quota this key has spent. `null` when the rate-limit store or the usage log could not be read; the key itself is still returned. The current-window numbers are read from the same counter the rate limiter charges, so they equal the `RateLimit-*` headers the key's own calls receive. Calling this endpoint never charges the user's key.
    - `window_sec` integer, required: Length of the quota window in seconds (3600 in production).
    - `units_limit` integer, nullable, required: Units per window. null = the key is not metered; then units_used, units_remaining and reset_at are null too.
    - `units_used` integer, nullable, required: Units spent in the current window, rejected (429) requests included, as the limiter counts them.
    - `units_remaining` integer, nullable, required: max(0, units_limit - units_used). Same number as `RateLimit-Remaining`.
    - `reset_at` string, date-time, nullable, required: End of the current window, UTC. The window is fixed, not rolling: the whole quota comes back at this instant.
    - `used_24h` integer, required: Units spent in the last 24 hours, from usage accounting. Trails live traffic by up to ~15 s.
    - `used_30d` integer, required: Units spent in the last 30 days, from usage accounting.
    - `last_used_at` string, date-time, nullable, required: Start of the minute (UTC) of the key's most recent request, or null if it was never used.

## Other responses

- `201`: A new key was issued (`reissued: false`).
- `400`: `invalid_request`: unknown field, missing or malformed `external_id`, `create` not a boolean.
- `401`: A credential was sent and did not check out (`invalid_signature`, `timestamp_out_of_window`, ...).
- `403`: `key_issuer_required`: not an allowlisted service, or not HMAC-signed.
- `404`: `no_key`: `create: false` and this user has no key. Nothing was created.
- `409`: `key_revoked`: this user's key was revoked or expired, whatever `create` says. Nothing new is issued.
- `429`: `daily_issue_limit`: the endpoint-wide budget of new keys for the last 24 hours is spent. Body carries `limit`, `window_sec` (86400) and `retry_after`; a `Retry-After` header too.
- `500`: `issuance_failed`.
- `503`: `issuance_paused`: new keys are switched off; existing ones are still returned.

---

[API](https://skmtc.dev/publicaml/apis/publicaml-intelligence-api.md) · [All operations](https://skmtc.dev/publicaml/apis/publicaml-intelligence-api/llms.txt) · [OpenAPI document](https://skmtc.dev/publicaml/apis/publicaml-intelligence-api/revisions/7c0c6315088c?raw)
