API
The v1 API
One credential, a versioned REST surface, and a machine-readable contract generated from the code that enforces it.
Who can use it today
One administrator, holding one token. There is no signup and no account system, so there is no self-serve way to obtain a credential yet — the surface is documented here because it is real and running, not because you can sign up for it. Links you create through the public form stay anonymous and unowned: nothing in the API can list, read, edit or report on them.
Authentication
API clients send the token in an Authorization header, as a Bearer credential. Every failure — no token, a malformed one, an unknown one, a revoked one, an expired one — answers with exactly the same 401 and the same body, because an endpoint that tells them apart tells an attacker which guess was once real.
curl https://gecis.in/api/v1/links \
-H "Authorization: Bearer gcs_live_…"The admin panel does not keep the token. It posts it once to the session endpoint; the server verifies it and replies with an httpOnly, Secure, SameSite=Lax cookie that no script on the page can read. The session slides forward while it is being used, stops at a hard ceiling whatever it has been doing, and logging out revokes it in the database rather than asking the browser to forget it.
curl -X POST https://gecis.in/api/v1/auth/session \
-H "Content-Type: application/json" \
-d '{"token":"gcs_live_…"}' -c cookies.txtA token carries 256 bits of entropy behind a fixed, self-identifying prefix, so one that leaks into a commit or a screenshot is recognisable to a secret scanner that has never heard of us. Only a SHA-256 digest of the whole string is stored: the value is shown once, when it is created, and no query anywhere can produce it again.
- entropy
- 256 bit
- prefix
- gcs_live_…
- stored as
- SHA-256
- session idle
- 30 min
- session ceiling
- 12 h
Limits
Per token, per hour, across the whole surface — with a tighter ceiling on link creation. Token verification is limited separately and much harder, by client address, and that lockout lives in the database rather than in memory so that restarting the process does not hand the attempts back. Every response carries the remaining allowance in its headers, and a refusal carries Retry-After.
- requests / token / hour
- 1000
- link creates / token / hour
- 300
- session attempts / address / hour
- 20
- lockout
- 10 / 15 min
Endpoints
| Method | Path | What it does |
|---|---|---|
| POST | /api/v1/auth/session | Exchange a token for a panel session cookie |
| GET | /api/v1/auth/session | Describe the credential this request used |
| DELETE | /api/v1/auth/session | Log out, and revoke the session server-side |
| GET | /api/v1/links | List your links, paginated, filtered and sorted |
| POST | /api/v1/links | Create one link, with an optional custom slug |
| POST | /api/v1/links/bulk | Create many links at once, each with its own outcome |
| GET | /api/v1/links/{slug} | Read one link |
| PATCH | /api/v1/links/{slug} | Change a link’s destination or its active flag |
| DELETE | /api/v1/links/{slug} | Delete a link, and the clicks recorded against it |
| GET | /api/v1/links/{slug}/analytics | Aggregates for one link |
| GET | /api/v1/analytics | Aggregates across every link you own |
| GET | /api/v1/links/{slug}/qr | A QR code for a link, as PNG or SVG |
| GET | /api/v1/tokens | List tokens, with their state and never their value |
| POST | /api/v1/tokens | Mint a token, shown once and never again |
| DELETE | /api/v1/tokens/{id} | Revoke a token, and every session it opened |
| GET | /api/v1/openapi.json | This surface, as an OpenAPI document |
Errors
One envelope everywhere: a stable code a client can branch on, and an English sentence for whoever is reading the log. The sentences are deliberately not translated — they are part of the contract, not page copy — and validation failures add a list of the offending fields. No error body ever contains a credential, a hash or an address.
| Code | Status | Message |
|---|---|---|
| invalid_body | 400 | The request body is not valid JSON. |
| validation_failed | 400 | One or more fields are invalid. |
| invalid_query | 400 | One or more query parameters are invalid. |
| invalid_target | 400 | The destination URL was refused. |
| invalid_slug | 400 | The requested slug is not a valid slug. |
| unauthorized | 401 | A valid API credential is required. |
| not_found | 404 | No such resource. |
| method_not_allowed | 405 | That method is not supported here. |
| slug_taken | 409 | That slug is already in use. |
| slug_reserved | 409 | That slug is reserved and cannot be allocated. |
| slug_collision | 409 | Could not allocate a free slug; retry. |
| payload_too_large | 413 | The request body is too large. |
| unsupported_media_type | 415 | Send application/json. |
| rate_limited | 429 | Too many requests. Retry after the given delay. |
| server_error | 500 | The request could not be completed. |
| unavailable | 503 | The database is unavailable. |
What the API will not return
No endpoint returns a visitor IP address, at any level of aggregation, and there is no endpoint that returns a click row at all — everything is a count. This is a published claim on the comparison table, where it is a point of contrast against vendors whose click objects carry the address, so it is enforced in the code rather than left as an intention: the dimension list has no entry for it, and a test fails if any response grows one.
Captured, but never a bucket you can group by
clicked_atServed as the time series insteadipIdentity. Never exposed, at any aggregationip_hashIdentity. Never exposed, at any aggregationvisitor_keyCounted, never returned or grouped onlatitudeCity centroid; the city is the honest bucketlongitudeCity centroid; the city is the honest bucket
The machine-readable contract
The OpenAPI document is generated at request time from the very schemas the handlers validate with, so a field renamed in the code is renamed in the document. Nothing in it is written by hand, and a route with no documented operation — or an operation with no route — fails the test suite.
Open the OpenAPI document