Keys
Keys endpoints to create, list, get, and revoke the SDK keys and API keys that authenticate requests to WarpLink, including tying an SDK key to one app.
Every request to the WarpLink API authenticates with a key issued through this endpoint set. A key belongs to exactly one organization, is created once, and shows its raw value exactly once: in the response to the create call. From then on, the server stores only a SHA-256 hash of it, so nothing in the database, in logs, or in a backup ever holds the plaintext again. Everything else in this API, and every SDK running on a device, is authenticated by one of these keys.
There are two kinds of key, distinguished by the type field on creation, and they answer different needs. An api key is what you choose the scopes for yourself, meant for backend scripts, CI, MCP clients, and anything a human or a service controls directly. An sdk key is pre-scoped with exactly links:read and attribution:write, meant to be embedded in a mobile app's WarpLink.configure() call, and it never expires, because an app you've already shipped can't prompt anyone to rotate a credential. Both key types share the same wl_live_ prefix followed by 32 alphanumeric characters, so they're indistinguishable by sight; the dashboard's SDK keys and API keys tabs are where you tell them apart. See Authentication for the full comparison of the two key types.
Authentication and Permissions
Managing keys, meaning creating, listing, reading, or deleting any of them, is gated the same way regardless of which operation you're calling:
| Operation | Scope | Role required (dashboard-created keys) |
|---|---|---|
| Create a key | keys:write | owner or admin (keys:manage) |
| List keys | keys:read | owner or admin (keys:manage) |
| Get a key | keys:read | owner or admin (keys:manage) |
| Delete a key | keys:write | owner or admin (keys:manage) |
There is no keys:manage scope on the key itself; that name belongs to the role-permission matrix, checked in addition to the scope for any key that was created through the dashboard UI and therefore carries a created_by. A member role never holds keys:manage, so even a key with keys:read scope that a member created cannot list or read other keys. Key management in WarpLink is an admin-and-above surface, the same as team invitations and app configuration. A key created without dashboard context (created_by is null) skips the role check and is authorized on scope alone, which keeps keys created directly through the API working exactly as they always have.
Validation and Behavior
typeisapiorsdk. Omitting it defaults toapi, for backward compatibility with clients that predate the field.- An
apikey requiresname(1 to 100 characters) andscopes(at least one, from the requestable set). Requestinglinks:writewithoutlinks:readaddslinks:readautomatically, so a key that can write links can always read them back. attribution:writeis not in the requestable scope list. It is reserved forsdkkeys and auto-granted to them; asking for it on anapikey is rejected the same as any other invalid scope value.- An
sdkkey requires onlyname. Its scopes (links:read,attribution:write) and its lack of an expiry are fixed by the server, not by the request body. expires_at, valid only onapikeys, must be an ISO 8601 date strictly in the future.sdkkeys cannot be given an expiry at all.app_idis valid only onsdkkeys. It ties the key to one app of your organization: the key then works only with the WarpLink SDK and only as that app. Omit it, or sendnull, for an SDK key that works for any app in the organization. On anapikey any non-nullapp_idis rejected with400 VALIDATION_ERROR, anddetails.fieldscontains{ "field": "app_id", "message": "API keys cannot be tied to an app" }.- An
app_idthat is not an app of your organization is rejected with400 VALIDATION_ERROR, anddetails.fieldscontains{ "field": "app_id", "message": "App not found" }. The message is the same for an unknown id and for another organization's app, so it cannot be used to look up app ids. - A key's app cannot be changed after creation. To use a key for another app, create a new SDK key and delete the old one. Deleting an app deletes the SDK keys tied to it.
- An SDK key, tied to an app or not, is accepted only on the SDK endpoints (
/v1/sdk/validate,/v1/links/resolve/{slug}and/v1/attribution/match). Every other endpoint, including this one, and the MCP server answer it with403 SDK_KEY_ROUTE_FORBIDDEN. On attribution, a request from a different app gets403 KEY_APP_MISMATCH. - The raw key format is
wl_<environment>_followed by 32 base62 characters. Every key created through this API is a live key; there is no way to request awl_test_key here. - There is no plan limit on the number of keys an organization can hold. Every plan, including Free, can create as many keys as it needs.
Create a Key
/keysCreate a key
Request Body
| Field | Type | Description |
|---|---|---|
type | "api" | "sdk" | `sdk` creates a mobile SDK key with fixed scopes that never expires. `api` is the default. |
name* | string | |
app_id | string | null | Only for `sdk` keys. Ties the key to one app of your organization, so it works only with the SDK and only for that app. Omit or send null for a key that works for any app. An `api` key cannot be tied to an app. An id that is not an app of your organization is a validation error. |
scopes | "links:read" | "links:write" | "apps:read" | "apps:write" | "analytics:read" | "analytics:write" | "keys:read" | "keys:write" | "domains:read" | "domains:write" | "org:read" | "team:read" | "team:write"[] | Required for `api` keys. Ignored for `sdk` keys, whose scopes are fixed. |
expires_at | string | Must be in the future. |
Responses
201Key created. The `raw_key` field is only returned once.
400Validation error
| Field | Type | Description |
|---|---|---|
error* | object |
401Unauthorized
| Field | Type | Description |
|---|---|---|
error* | object |
403Forbidden
| Field | Type | Description |
|---|---|---|
error* | object |
The raw_key field in this response is the only place the plaintext key ever appears. Every other response in this API, including the immediate GET of the same key, returns key_prefix (wl_live_) instead, which is enough to tell keys apart in a list but not enough to authenticate with. If you lose the raw value, there's no recovery path other than deleting the key and creating a new one.
List Keys
/keysList keys
Responses
200List of API keys
| Field | Type | Description |
|---|---|---|
data | object[] |
401Unauthorized
| Field | Type | Description |
|---|---|---|
error* | object |
403Forbidden
| Field | Type | Description |
|---|---|---|
error* | object |
Returns every key in the organization, both api and sdk types, ordered as stored. Every item carries app_id: the app an sdk key is tied to, or null for api keys and for sdk keys that work for any app. There is no pagination on this endpoint and no filter by type; an organization managing many keys filters client-side on the type field in the response.
Get a Key
/keys/{id}Get a key
Parameters
| Name | Type | In | Description |
|---|---|---|---|
id* | string | path |
Responses
200API key details
| Field | Type | Description |
|---|---|---|
id | string | |
type | "api" | "sdk" | |
key_prefix | string | e.g. wl_live_ |
name | string | |
app_id | string | null | The app an `sdk` key is tied to. Null for `api` keys and for `sdk` keys that work for any app. |
scopes | string[] | |
last_used_at | string | null | |
expires_at | string | null | |
created_at | string | |
updated_at | string |
401Unauthorized
| Field | Type | Description |
|---|---|---|
error* | object |
403Forbidden
| Field | Type | Description |
|---|---|---|
error* | object |
404Not found
| Field | Type | Description |
|---|---|---|
error* | object |
Returns one key's metadata by its id, the same shape as an item in the list response. This is a metadata read, not an authentication check; it never returns or confirms a raw key value, and a request against a key belonging to another organization is refused with 404 NOT_FOUND rather than 403, so a caller can't use this endpoint to discover which key IDs exist elsewhere.
Delete a Key
/keys/{id}Delete a key
Parameters
| Name | Type | In | Description |
|---|---|---|---|
id* | string | path |
Responses
204Deleted
401Unauthorized
| Field | Type | Description |
|---|---|---|
error* | object |
403Forbidden
| Field | Type | Description |
|---|---|---|
error* | object |
404Not found
| Field | Type | Description |
|---|---|---|
error* | object |
Deleting is permanent and immediate. The key's hash no longer matches on lookup the moment the row is gone, so any request still using that key, including one already in flight, fails with 401 UNAUTHORIZED from that point on, indistinguishable from a key that never existed. There is no separate "revoke" state to reactivate from; deleting an sdk key embedded in a shipped app breaks attribution and deep link resolution for every install still using it, so rotate before you delete when a mobile app depends on the key.
Worked Example
Create a scoped API key for a CI pipeline, confirm it by listing keys, then delete it once the pipeline is retired:
curl -X POST https://api.warplink.app/v1/keys \
-H "Authorization: Bearer wl_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "CI/CD Key",
"scopes": ["links:read", "links:write"],
"expires_at": "2027-12-31T00:00:00.000Z"
}'
{
"id": "5c1e2b3a-...",
"type": "api",
"app_id": null,
"key_prefix": "wl_live_",
"name": "CI/CD Key",
"scopes": ["links:read", "links:write"],
"last_used_at": null,
"expires_at": "2027-12-31T00:00:00.000Z",
"created_at": "2026-09-16T12:00:00.000Z",
"updated_at": "2026-09-16T12:00:00.000Z",
"raw_key": "wl_live_9fQ2xR7pL4nB1cVwYtKzJmH8aSdEo0Ui"
}
Creating an SDK key for one mobile app looks the same, minus the scopes and expiry, plus the app it is tied to:
curl -X POST https://api.warplink.app/v1/keys \
-H "Authorization: Bearer wl_live_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "type": "sdk", "name": "iOS Production", "app_id": "9d4f1a20-..." }'
{
"id": "7a2b9c10-...",
"type": "sdk",
"app_id": "9d4f1a20-...",
"key_prefix": "wl_live_",
"name": "iOS Production",
"scopes": ["links:read", "attribution:write"],
"last_used_at": null,
"expires_at": null,
"created_at": "2026-09-16T12:00:00.000Z",
"updated_at": "2026-09-16T12:00:00.000Z",
"raw_key": "wl_live_bK3nQpXo9rTzYwLmH2vCsJaFd6UeIg1O"
}
curl -X DELETE https://api.warplink.app/v1/keys/5c1e2b3a-... \
-H "Authorization: Bearer wl_live_YOUR_API_KEY"
The delete call returns 204 No Content on success, with no body to parse.
Common Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | Missing name, empty scopes, an invalid scope value, an expires_at that isn't in the future, an app_id on an api key, or an app_id that is not an app of your organization |
| 400 | INVALID_ID | The id path parameter isn't a valid UUID |
| 401 | UNAUTHORIZED | Missing, invalid, or expired API key on the request itself |
| 403 | FORBIDDEN | The calling key lacks keys:read or keys:write, or its creator's role lacks keys:manage |
| 403 | SDK_KEY_ROUTE_FORBIDDEN | The calling key is an SDK key. SDK keys can only call the SDK endpoints. Use an API key for this request |
| 404 | NOT_FOUND | No key with that ID exists in this organization |
FAQ
Can I tie an SDK key to one app?
Yes. Send app_id when you create an sdk key. The key then acts only as that app. You cannot change it later, and api keys cannot be tied. Tied or not, an SDK key can call only the SDK endpoints.
Can I see a key's raw value again after creation?
No. raw_key appears exactly once, in the create response. Every later read, including GET /v1/keys/{id}, returns key_prefix only.
What's the difference between an api key and an sdk key?
An api key carries whatever scopes you request and can carry an expiry; an sdk key is fixed to links:read and attribution:write, never expires, and is the only credential type that can record install attribution. Putting an api key into a mobile app's configure() call resolves deep links fine but silently fails attribution, since it can't hold attribution:write.
Can I request attribution:write on an api key?
No. It's excluded from the requestable scope list entirely and is only ever granted automatically, to sdk keys.
Is there a limit on how many keys I can create? No. Key count isn't one of the plan-gated resources; every plan, including Free, can create as many keys as needed.
Can a member role create or manage API keys?
No. Key management requires the keys:manage permission, which only owner and admin roles hold, even for a read-only GET request with a properly scoped key.