Moving to public IDs (UUIDs)
Moving to public IDs (UUIDs)
The ngris API now identifies every object you can address by a uuid instead of an internal integer id. Tunnels, API keys, auth tokens, rate-limit rules, traffic policies, routing rules, RBAC clients and roles, members, invitations, SSO configs, client CAs, dedicated IPs, error templates, traffic logs and more each carry a uuid. Every reference to another object gains a <thing>_uuid field next to the old <thing>_id.
Integer ids are deprecated. They keep working during a 30-day window, then the API refuses them. Endpoints, domains, certificates and applications already used uuids in their URLs; nothing changes there except that the URLs now accept a uuid only.
- Paths take the object's
uuid:/v1/tunnels/{uuid},/v1/keys/auth/{uuid},/v1/firewall/rate-limits/{uuid}. - Request bodies and query parameters take
<thing>_uuid(endpoint_uuid,role_uuid,ca_uuids) instead of<thing>_id. - Plans have no uuid: they are addressed by their slug. Send
"plan": "team"instead ofplan_id; responses carryplannext to everyplan_id. - Responses carry
uuidon every object and<thing>_uuidnext to every reference. No existing field changes type; the integers are removed at the sunset.
/v1, /api/v1 and /api alike. There is no /v2; this is a change inside v1 with a deprecation window.curl -X DELETE "https://api.ngris.com/v1/keys/auth/1287" \
-H "X-API-KEY: $NGRIS_API_KEY"
# 1. Read the token's uuid from the list
curl -s "https://api.ngris.com/v1/keys/auth" \
-H "X-API-KEY: $NGRIS_API_KEY" | jq '.items[] | {uuid, description}'
# 2. Address it by uuid
curl -X DELETE "https://api.ngris.com/v1/keys/auth/019ae915-a554-7fc4-aea3-9c9ce2dd2976" \
-H "X-API-KEY: $NGRIS_API_KEY"
Timeline
| Date | What happens |
|---|---|
| 2026-10-14 | Every route, body and query parameter accepts the uuid; every response carries uuid and the *_uuid fields. Integer ids still work, with deprecation headers. |
| 2026-11-13 23:59:59 UTC | The sunset. After this moment an integer id is refused with 400 integer_id_unsupported, and the integer id / *_id fields are removed from responses. |
Sunset header on every deprecated request always carries the current date, and we announce any change on this page. If your requests never get the deprecation headers, an earlier sunset doesn't affect you.# A request with an integer id gets the deprecation headers
curl -s -o /dev/null -D - \
"https://api.ngris.com/v1/tunnels/4521" \
-H "X-API-KEY: $NGRIS_API_KEY" | grep -iE '^(deprecation|sunset|link):'
Deprecation headers
Until the sunset, every request that sends an integer id where a uuid is expected, in the path, the body or the query string, still succeeds. Its response carries three headers:
Deprecation(RFC 9745):@followed by the Unix time the deprecation took effect.Sunset(RFC 8594): the HTTP date after which integer ids are refused.Link: this page, withrel="deprecation".
Requests that use uuids only never get these headers. Log the headers in your client to find the calls you still have to move. Each such request is also counted on our side, which is how we know when integer use has stopped.
HTTP/2 200
content-type: application/json
deprecation: @1792022399
sunset: Fri, 13 Nov 2026 23:59:59 GMT
link: <https://ngris.com/docs/public-ids>; rel="deprecation"; type="text/html"
After the sunset
An integer id sent after the sunset, in a path, a body or a query string, is answered with 400 and the code integer_id_unsupported. The param field names the parameter that carried the integer.
Routes that already took a uuid (endpoints, domains, certificates, applications, firewall policies, metric alerts) accept a uuid only. Anything else in that path segment, an integer included, matches no route and gets a plain 404.
The integer id and *_id fields disappear from responses at the same time. Code that still reads them gets nothing.
{
"error": "Integer ids are no longer accepted. Use the resource's uuid (returned as \"uuid\" in every response).",
"code": "integer_id_unsupported",
"param": "id"
}
Paths
These routes took an integer and now take the object's uuid. The integer still works until the sunset, with the deprecation headers.
| Route | Takes |
|---|---|
/user/sessions/{uuid} | your session's uuid |
/user/invitations/{uuid}/accept, /decline | invitation uuid |
/account/members/{user_uuid}, …/role | the member's user uuid (user_uuid in the member list) |
/account/invites/{uuid}, /account/roles/{uuid} | invitation uuid, role uuid |
/stats/users/{uuid} | your own user uuid |
/keys/api/{uuid}, /keys/auth/{uuid} | API key uuid, auth token uuid |
/tunnels/{uuid}, …/tags, /stats/tunnels/{uuid} | tunnel uuid |
/traffic/endpoints/{tunnel_uuid}/metrics | the tunnel uuid (the route's historical name) |
/endpoints/{uuid}/oauth/providers/{provider_uuid} | OAuth provider uuid |
/endpoints/{uuid}/rbac/clients/{client_uuid}, …/sessions/{session_uuid}/revoke | RBAC client uuid; client session uuid |
/endpoints/{uuid}/rbac/roles/{role_uuid} | RBAC role uuid |
/endpoints/{uuid}/policies/{policy_uuid}, …/rules/{rule_uuid} | traffic policy uuid; policy rule uuid |
/endpoints/{uuid}/routing-rules/{rule_uuid} | routing rule uuid |
/firewall/rate-limits/{uuid}, …/conditions/{condition_uuid} | rate-limit rule uuid; condition uuid |
/traffic/logs/{uuid}, …/replay, …/analyze | traffic log uuid |
/templates/{uuid} | error template uuid |
/sso/config/{uuid} | SSO config uuid |
/dedicated-ips/{uuid}/release | dedicated-IP assignment uuid (an item of GET /dedicated-ips) |
/dedicated-ips/groups/{uuid}/release, /dedicated-ips/requests/{uuid}/cancel | group uuid, request uuid |
/client-cas/{uuid} | client CA uuid |
/git/installations/{uuid}, …/repos | the linked installation's uuid (not GitHub's installation_id) |
/billing/balance/topups/{uuid}/receipt.pdf | ledger row uuid |
/public/incidents/{uuid}, /public/maintenances/{uuid} | incident / maintenance uuid |
A child object resolves only under the parent named in the path: a policy rule under its policy, a client session under its client, a rate-limit condition under its rule.
# Rate-limit rules: uuid next to name
curl -s "https://api.ngris.com/v1/firewall/rate-limits" \
-H "X-API-KEY: $NGRIS_API_KEY" | jq '.rules[] | {uuid, name}'
# Toggle one by uuid
curl -X PUT "https://api.ngris.com/v1/firewall/rate-limits/019b344f-6477-7b07-8dac-7d370474eab1/toggle" \
-H "X-API-KEY: $NGRIS_API_KEY"
Request bodies and query parameters
Send the new name. The old name still works until the sunset, with the deprecation headers.
| Route | Send | Instead of (deprecated) |
|---|---|---|
POST /user/switch-account | account_uuid | account_id |
PUT /account/members/{user_uuid}/role, POST /account/invites | role_uuid | role_id |
POST /account/transfer-owner | user_uuid, old_owner_role_uuid | user_id, old_owner_role_id |
POST /keys/auth | endpoint_uuid, tunnel_uuid | endpoint_id, tunnel_id |
POST /endpoints, POST /certificates/self, GET /endpoints?… | domain_uuid | domain_id |
PUT /endpoints/{uuid}/certificate | certificate_uuid | certificate_id |
POST /applications (GitHub import) | git_installation_uuid | git_installation_id |
POST /billing/checkout, GET /billing/preview-plan-change?… | plan (the slug) | plan_id |
POST / DELETE /endpoints/{uuid}/rbac/memberships | client_uuid, role_uuid | client_id, role_id |
PUT /endpoints/{uuid}/rbac/registration-policy | default_role_uuid | default_role_id |
POST /endpoints/{uuid}/routing-rules/reorder, POST /firewall/policies/reorder | ordered_uuids | ordered_ids |
PUT /endpoints/{uuid}/mtls | ca_uuids | ca_ids |
POST /endpoints/{uuid}/dedicated-ip, …/dedicated-ip-group | assignment_uuid, group_uuid | assignment_id, group_id |
POST /account/objectives | endpoint_uuid | endpoint_id |
GET /firewall/rate-limits?…, GET /traffic/metrics?…, GET /traffic/security-analytics?… | endpoint_uuid | endpoint_id |
GET /traffic/metrics/compare?… | endpoint_uuids (repeat it) | endpoint_ids |
GET /traffic/logs, …/export, POST /traffic/smart-search | endpoint_uuid, rate_limit_rule_uuid, since_uuid, policy_uuid | endpoint_id, rate_limit_rule_id, since_id, policy_id |
- A malformed uuid is
400:{"error": "endpoint_uuid must be a uuid", "param": "endpoint_uuid"}. - A uuid you can't address, including another account's, is
404, worded exactly like an unknown object. - Both names sent for different objects is
400 id_uuid_mismatch. For arrays, the two lists must name the same objects in the same order. Send the uuid only. - Bodies that reject unknown fields accept the new names.
POST /v1/keys/auth
{
"description": "ci-bot agent",
"endpoint_id": 184
}
PUT /v1/endpoints/{uuid}/mtls
{ "mode": "required", "ca_ids": [7, 12] }
POST /v1/billing/checkout
{ "plan_id": 2, "interval": "monthly" }
POST /v1/keys/auth
{
"description": "ci-bot agent",
"endpoint_uuid": "019bb1e4-e785-77d2-9966-c3cff31d0a0e"
}
PUT /v1/endpoints/{uuid}/mtls
{
"mode": "required",
"ca_uuids": [
"019af8a2-4b16-70ea-8090-9f63343250c8",
"019b07e7-fbab-74be-a38d-e60284dd7254"
]
}
POST /v1/billing/checkout
{ "plan": "team", "interval": "monthly" }
{
"error": "endpoint_id and endpoint_uuid name different objects; send endpoint_uuid only",
"code": "id_uuid_mismatch",
"param": "endpoint_uuid"
}
Responses
- Every addressable object carries
uuid. - Every reference
<thing>_idgains<thing>_uuidnext to it; arrays gain<thing>_uuidsin the same order. A*_uuidfield is omitted when the reference is empty. - Every
plan_idgainsplan(the slug);GET /planslists each plan'sslug. - Firewall policies and conditions keep their string
id(it already was a uuid) and gainuuidwith the same value. - Rows that only appear in lists and that nothing addresses carry no uuid, for example account membership rows, activity and audit-log entries, metric-alert events, incident updates and tunnel agents. Their integer
idis removed at the sunset.
Until the sunset responses carry both forms. Switch your code to the uuid fields now; the integer fields are deprecated and disappear at the sunset.
Still integers, by design
These are not our internal ids and don't change: Stripe ids (stripe_*, payment_method_id, invoice and tax ids), the agent's own agent_id, request_id and connection_id, the DNS provider's record_id, GitHub's installation_id and repository ids, and counts, limits, ports and priorities.
{
"items": [
{
"id": 1287,
"uuid": "019ae915-a554-7fc4-aea3-9c9ce2dd2976",
"user_id": 1024,
"user_uuid": "019a52c2-ca1e-78ba-ab5c-50362cc29134",
"endpoint_id": 184,
"endpoint_uuid": "019bb1e4-e785-77d2-9966-c3cff31d0a0e",
"description": "ci-bot agent",
"status": "active"
}
]
}
{
"items": [
{
"uuid": "019ae915-a554-7fc4-aea3-9c9ce2dd2976",
"user_uuid": "019a52c2-ca1e-78ba-ab5c-50362cc29134",
"endpoint_uuid": "019bb1e4-e785-77d2-9966-c3cff31d0a0e",
"description": "ci-bot agent",
"status": "active"
}
]
}
Other behaviour changes
- Another account's object is a 404. A uuid or integer of an object outside your active account is answered exactly like an unknown id. A few routes used to answer
403there and now answer404: the endpoint API-key routes, dedicated-IP release and group release, and client-CA delete. - Members: removing or re-roling a user who isn't a member of the account is
404 Member not found(it was400). - Case: uuids are accepted in upper or lower case everywhere. Domain, certificate and identity-verification routes used to accept lower case only.
- References are checked:
endpoint_uuid/tunnel_uuidonPOST /keys/auth,endpoint_uuidonPOST /account/objectivesanddefault_role_uuidon the RBAC registration policy must name an object you can address, otherwise the request is404. The legacy integers are checked the same way.
Webhooks, exports, emails and links
Ids also leave the API in webhooks, files and links. They move to uuids the same way:
- Alert webhooks: the body gains
endpoint_uuidnext toendpoint_id(rule_uuidwas already there).endpoint_idis removed at the sunset; readendpoint_uuid. Slack and Discord messages name the endpoint by its uuid. See Metrics & alerts. - Traffic-log export: each JSON entry gains
uuidand the CSV gains aUUIDcolumn (the last one, so no existing column moves). The JSONidand the CSVIDcolumn are removed at the sunset. See Export Traffic Logs. - Data export (
POST /user/data-export): every object carriesuuidand every reference<thing>_uuid; the integers are removed at the sunset. - Account invitation emails link to the invitation by its uuid. A link from an email sent earlier still works until the invitation expires (14 days): the dashboard forwards it to the uuid link.
- Status page: incidents and maintenances live at
/status/incidents/<uuid>and/status/maintenances/<uuid>. An older integer link redirects there until the sunset. - Per-region dedicated-IP groups publish their geo name as
grp-<group uuid>.<geo zone>(thegeo_fqdnfield). - Support desk: ticket messages name other tickets by uuid, and campaign emails link through the link's uuid; links in emails sent earlier keep working.
{
"rule_uuid": "019c2f7a-1d3e-7b40-9a6c-5e8f0b2d4c71",
"name": "API p95 latency",
"status": "firing",
"metric": "p95_latency_ms",
"operator": ">",
"threshold": 500,
"value": 742.5,
"window_seconds": 300,
"observed_at": "2026-10-13T09:00:00Z",
"endpoint_id": 184,
"endpoint_uuid": "019bb1e4-e785-77d2-9966-c3cff31d0a0e",
"test": false
}
MCP server, Python SDK and Kubernetes operator
MCP server
Tools take and return uuids. A numeric id input is accepted as a deprecated alias until the sunset. Assistants re-read tool schemas every session, so nothing to do there; only saved prompts or scripts that quote a numeric id need the uuid instead. See MCP Server.
Python SDK
From ngris 0.2.0 every method takes and returns string uuids: client.tunnels.get(tunnel_uuid), client.api_keys.delete_auth_token(token_uuid), client.billing.checkout(plan="team"), client.endpoints.assign_certificate(endpoint_uuid, certificate_uuid=…). Models expose uuid and the *_uuid fields; their integer fields are optional and deprecated. An integer passed to a method still works until the sunset and emits a DeprecationWarning. Upgrade with pip install -U ngris before the sunset.
Kubernetes operator
From operator 0.2.0 the custom resources record the ngris objects they own by uuid. Resources created by an older operator still hold an integer id in their status; on its next reconcile the new operator looks up the matching uuid and stores it. That lookup needs the integer ids, so upgrade the operator before the sunset.
Dashboard and the agent
Nothing to do in the dashboard. Agent auth token values don't change, so nothing changes on the machines that run the agent.
from ngris import Ngris
client = Ngris(api_key="<your_api_key>")
tok = client.api_keys.create_auth_token(
description="ci-bot agent",
endpoint_uuid="019bb1e4-e785-77d2-9966-c3cff31d0a0e",
)
print(tok["uuid"])
client.api_keys.delete_auth_token(tok["uuid"])
client.billing.checkout(plan="team")
Migration checklist
- Find the calls that still send integers: log the
DeprecationandSunsetresponse headers, or search your code for_idfields and numeric path segments. - Read the uuid of each object from its list or create response (
uuid), and store it instead of the integer. - Build paths from the uuid; send
*_uuidfields andplanslugs in bodies and query strings. - Read
uuid/*_uuidfrom responses; stop readingid/*_id. - Upgrade the Python SDK and the Kubernetes operator to 0.2.0 or later.
- Done when none of your requests gets the deprecation headers any more, before 2026-11-13 23:59:59 UTC.
Every page of the API reference shows the uuid form. Questions: contact us.