Add subscription
const url = 'https://app.tunnelhq.com/api/v1/subscriptions';const options = { method: 'POST', headers: { 'X-Project-Id': 'prj_12', 'X-API-Key': '<X-API-Key>', 'Content-Type': 'application/json' }, body: '{"url":"https://provider.your-vpn.net/sub/<token>","name":"Provider feed","syncIntervalSec":1800}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}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.
Authorizations
Section titled “Authorizations”Parameters
Section titled “ Parameters ”Header Parameters
Section titled “Header Parameters”The project, as prj_12 or 12. A workspace key can only reach its own workspace’s projects.
Example
prj_12Request Bodyrequired
Section titled “Request Bodyrequired”object
The subscription URL. It must reach a public address.
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.
Seconds each server’s test may take.
Servers tested at the same time.
Send the JSON value false; the string "false" counts as true.
Update this subscription instead of adding one.
Example
{ "url": "https://provider.your-vpn.net/sub/<token>", "name": "Provider feed", "syncIntervalSec": 1800}Responses
Section titled “ Responses ”The project already had this URL, so it was updated.
A subscription URL’s settings and last sync. The URL itself isn’t returned. Note the camelCase field names.
object
Seconds between syncs.
Seconds each server’s test may take.
Servers tested at the same time.
The engines tried on each config; both unless changed in the dashboard.
In UTC, as YYYY-MM-DD HH:MM:SS. null before the first sync.
Servers found in the last sync.
auto_disabled means TunnelHQ switched the subscription off after it kept failing.
In UTC, as YYYY-MM-DD HH:MM:SS.
In UTC, as YYYY-MM-DD HH:MM:SS.
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"}Headers
Section titled “Headers”Requests the workspace’s plan allows per minute.
Requests left in the current minute.
Unix time, in seconds, when the current minute ends.
Added. The first sync starts on the next poll.
A subscription URL’s settings and last sync. The URL itself isn’t returned. Note the camelCase field names.
object
Seconds between syncs.
Seconds each server’s test may take.
Servers tested at the same time.
The engines tried on each config; both unless changed in the dashboard.
In UTC, as YYYY-MM-DD HH:MM:SS. null before the first sync.
Servers found in the last sync.
auto_disabled means TunnelHQ switched the subscription off after it kept failing.
In UTC, as YYYY-MM-DD HH:MM:SS.
In UTC, as YYYY-MM-DD HH:MM:SS.
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"}Headers
Section titled “Headers”Requests the workspace’s plan allows per minute.
Requests left in the current minute.
Unix time, in seconds, when the current minute ends.
The URL is missing, malformed, or not public.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Examples
{ "error": true, "code": 400, "message": "Missing or invalid url"}{ "error": true, "code": 400, "message": "Invalid URL format"}{ "error": true, "code": 400, "message": "URL hostname resolves to a private/reserved IP address"}{ "error": true, "code": 400, "message": "URL hostname could not be resolved"}The API key is missing, wrong, expired, switched off, or no longer valid.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Examples
{ "error": true, "code": 401, "message": "API key is required. Provide X-API-Key header or Authorization: Bearer <key>"}{ "error": true, "code": 401, "message": "Invalid or expired API key"}{ "error": true, "code": 401, "message": "API key has expired"}{ "error": true, "code": 401, "message": "API key owner account is inactive"}{ "error": true, "code": 401, "message": "This API key's workspace access has been removed"}The role or the plan doesn’t allow it.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Examples
{ "error": true, "code": 403, "message": "You do not have permission to edit this project"}{ "error": true, "code": 403, "message": "Subscription URLs are not available on your plan. Upgrade to use this feature."}{ "error": true, "code": 403, "message": "Subscription URL limit reached (3/3). Upgrade your plan."}The id you sent belongs to another project, or the project doesn’t exist.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Examples
{ "error": true, "code": 404, "message": "Subscription not found"}{ "error": true, "code": 404, "message": "Project not found or inactive"}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.
object
Seconds to wait. Absent for the monthly limit.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Examples
{ "error": "Rate limit exceeded", "message": "Per-minute limit of 60 requests exceeded", "retry_after": 60}{ "error": "Daily limit exceeded", "message": "Daily limit of 25000 requests exceeded", "retry_after": 3600}{ "error": "Monthly limit exceeded", "message": "Monthly limit of 250000 requests exceeded"}{ "error": true, "code": 429, "message": "Too many failed API key attempts from this address. Wait a few seconds and try again.", "retry_after": 6}Headers
Section titled “Headers”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.
object
The HTTP status, repeated.
What went wrong.
More detail, on some errors.
Seconds to wait, on the 429 for wrong keys.
Example
{ "error": true, "code": 500, "message": "Internal server error"}