Skip to content

Create an API key

POST
/keys
curl --request POST \
--url https://app.tunnelhq.com/api/v1/keys \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <X-API-Key>' \
--data '{ "name": "Staging CI", "expires": "2026-12-31T23:59:59Z" }'

Creates a key in the same workspace as the key making the request, and returns its secret once. The request must use a workspace key, from a workspace whose plan includes API access: the account-wide key from thq login is refused. The new key starts with uk.

Need an API key? See Getting an API key.

Media typeapplication/json
object
name
required
string
<= 120 characters
expires

When the key stops working. Left out, or not a date the server can read, the key never expires.

string format: date-time
Example
{
"name": "Staging CI",
"expires": "2026-12-31T23:59:59Z"
}

Created. key is the secret, shown only now.

Media typeapplication/json
object
id
required
integer
name
required
string
userID
required

The key’s owner.

integer
organizationId
required

The workspace the key belongs to; null for an account-wide key.

integer | null
createdDate
required

In UTC, as YYYY-MM-DD HH:MM:SS.

string
active
required

Whether the key is switched on: true/false when just created, 1/0 when listed.

boolean | integer
expires
required

When the key stops working, in UTC, as YYYY-MM-DD HH:MM:SS. null if never.

string | null
lastUsedAt
required
string | null
status
required

inactive means switched off.

string
Allowed values: active inactive expired
key
required

The full key. Store it now; it isn’t shown again.

string
Example
{
"id": 42,
"name": "Staging CI",
"userID": 7,
"organizationId": 3,
"createdDate": "2026-09-25 14:41:00",
"active": true,
"expires": "2026-12-31 23:59:59",
"lastUsedAt": null,
"status": "active",
"key": "uk42_<secret>"
}
X-RateLimit-Limit
integer

Requests the workspace’s plan allows per minute.

X-RateLimit-Remaining
integer

Requests left in the current minute.

X-RateLimit-Reset
integer

Unix time, in seconds, when the current minute ends.

The name is missing or too long.

Media typeapplication/json
object
error
required
boolean
code
required

The HTTP status, repeated.

integer
message
required

What went wrong.

string
details

More detail, on some errors.

retry_after

Seconds to wait, on the 429 for wrong keys.

integer
Examples
{
"error": true,
"code": 400,
"message": "Key name is required"
}

The API key is missing, wrong, expired, switched off, or no longer valid.

Media typeapplication/json
object
error
required
boolean
code
required

The HTTP status, repeated.

integer
message
required

What went wrong.

string
details

More detail, on some errors.

retry_after

Seconds to wait, on the 429 for wrong keys.

integer
Examples
{
"error": true,
"code": 401,
"message": "API key is required. Provide X-API-Key header or Authorization: Bearer <key>"
}

The request used an account-wide key, or the workspace’s plan has no API access.

Media typeapplication/json
object
error
required
boolean
code
required

The HTTP status, repeated.

integer
message
required

What went wrong.

string
details

More detail, on some errors.

retry_after

Seconds to wait, on the 429 for wrong keys.

integer
Example
{
"error": true,
"code": 403,
"message": "API access isn't included on this workspace's plan. Upgrade to create API keys."
}

A usage limit was reached, or this IP address made too many requests with a wrong key.

Usage limits are per workspace and plan: per minute (Retry-After: 60), per day (Retry-After: 3600), and per month (no Retry-After). They use their own body shape. An IP address may send 10 wrong keys a minute; one more is allowed every 6 seconds. While that allowance is used up, every request from the address is refused, even one with a valid key.

Media typeapplication/json
One of:
object
error
required
string
Allowed values: Rate limit exceeded Daily limit exceeded Monthly limit exceeded
message
required
string
retry_after

Seconds to wait. Absent for the monthly limit.

integer
Examples
{
"error": "Rate limit exceeded",
"message": "Per-minute limit of 60 requests exceeded",
"retry_after": 60
}
Retry-After
integer

Seconds to wait. 60 for the per-minute limit, 3600 for the daily limit, 6 after wrong keys. Absent for the monthly limit.

Something failed on TunnelHQ’s side.

Media typeapplication/json
object
error
required
boolean
code
required

The HTTP status, repeated.

integer
message
required

What went wrong.

string
details

More detail, on some errors.

retry_after

Seconds to wait, on the 429 for wrong keys.

integer
Example
{
"error": true,
"code": 500,
"message": "Internal server error"
}