Skip to content

Add subscription

POST
/subscriptions
curl --request POST \
--url https://app.tunnelhq.com/api/v1/subscriptions \
--header 'Content-Type: application/json' \
--header 'X-API-Key: <X-API-Key>' \
--header 'X-Project-Id: prj_12' \
--data '{ "url": "https://provider.your-vpn.net/sub/<token>", "name": "Provider feed", "syncIntervalSec": 1800 }'

Adds a provider’s subscription URL. TunnelHQ syncs it on its interval: each server in it becomes a monitor, and servers that disappear are paused. Needs a role that can edit the project (Owner, Admin, or Manager), and a plan that includes subscription URLs.

If the project already has this URL (or you send the id of one of its subscriptions), this updates it instead and answers 200. Any setting you leave out then goes back to its default. To change some settings only, use Update subscription.

Need an API key? See Getting an API key.

X-Project-Id
required
string

The project, as prj_12 or 12. A workspace key can only reach its own workspace’s projects.

Example
prj_12
Media typeapplication/json
object
url
required

The subscription URL. It must reach a public address.

string format: uri
name
string
syncIntervalSec

Seconds between syncs. The dashboard offers 5 minutes to 24 hours. It is also the check interval of the monitors the subscription creates: they are tested at each sync.

integer
default: 3600
timeoutSeconds

Seconds each server’s test may take.

integer
default: 30
maxWorkers

Servers tested at the same time.

integer
default: 5
enabled

Send the JSON value false; the string "false" counts as true.

boolean
default: true
id

Update this subscription instead of adding one.

integer
Example
{
"url": "https://provider.your-vpn.net/sub/<token>",
"name": "Provider feed",
"syncIntervalSec": 1800
}

The project already had this URL, so it was updated.

Media typeapplication/json

A subscription URL’s settings and last sync. The URL itself isn’t returned. Note the camelCase field names.

object
id
required
integer
projectId
required
integer
name
required
string | null
syncIntervalSec
required

Seconds between syncs.

integer
timeoutSeconds
required

Seconds each server’s test may take.

integer
maxWorkers
required

Servers tested at the same time.

integer
engine
required

The engines tried on each config; both unless changed in the dashboard.

string
enabled
required
boolean
lastSyncedAt
required

In UTC, as YYYY-MM-DD HH:MM:SS. null before the first sync.

string | null
lastServerCount
required

Servers found in the last sync.

integer | null
lastSyncStatus
required

auto_disabled means TunnelHQ switched the subscription off after it kept failing.

string | null
Allowed values: pending success failed auto_disabled
syncErrorMessage
required
string | null
createdAt
required

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

string
updatedAt
required

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

string
Example
{
"id": 3,
"projectId": 12,
"name": "Provider feed",
"syncIntervalSec": 1800,
"timeoutSeconds": 30,
"maxWorkers": 5,
"engine": "both",
"enabled": true,
"lastSyncedAt": "2026-09-25 14:00:05",
"lastServerCount": 14,
"lastSyncStatus": "success",
"syncErrorMessage": null,
"createdAt": "2026-09-01 09:12:44",
"updatedAt": "2026-09-25 14:00:05"
}
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.

Added. The first sync starts on the next poll.

Media typeapplication/json

A subscription URL’s settings and last sync. The URL itself isn’t returned. Note the camelCase field names.

object
id
required
integer
projectId
required
integer
name
required
string | null
syncIntervalSec
required

Seconds between syncs.

integer
timeoutSeconds
required

Seconds each server’s test may take.

integer
maxWorkers
required

Servers tested at the same time.

integer
engine
required

The engines tried on each config; both unless changed in the dashboard.

string
enabled
required
boolean
lastSyncedAt
required

In UTC, as YYYY-MM-DD HH:MM:SS. null before the first sync.

string | null
lastServerCount
required

Servers found in the last sync.

integer | null
lastSyncStatus
required

auto_disabled means TunnelHQ switched the subscription off after it kept failing.

string | null
Allowed values: pending success failed auto_disabled
syncErrorMessage
required
string | null
createdAt
required

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

string
updatedAt
required

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

string
Example
{
"id": 4,
"projectId": 12,
"name": "Provider feed",
"syncIntervalSec": 1800,
"timeoutSeconds": 30,
"maxWorkers": 5,
"engine": "both",
"enabled": true,
"lastSyncedAt": null,
"lastServerCount": 0,
"lastSyncStatus": "pending",
"syncErrorMessage": null,
"createdAt": "2026-09-25 14:40:00",
"updatedAt": "2026-09-25 14:40:00"
}
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 URL is missing, malformed, or not public.

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": "Missing or invalid url"
}

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 role or the plan doesn’t allow it.

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": 403,
"message": "You do not have permission to edit this project"
}

The id you sent belongs to another project, or the project doesn’t exist.

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": 404,
"message": "Subscription not found"
}

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"
}