Subscription Management¶
All endpoints on this page require the API-Token header (see Authentication) and are mounted at two parallel base paths that share identical request/response shapes:
- Self-service:
/api/v1/subs— operates on the calling user's own subscriptions. - Provider-scoped:
/api/v1/providers/{user_id}/subs— operates onuser_id's subscriptions, requires the caller to hold an approved connection for that user (see Provider API).
Everything below is written against the self-service path; substitute /api/v1/providers/{user_id}/subs wherever a provider needs to act on a specific user's behalf. Rate limit: internal_with_token_rps (default 3 req/sec) per client IP.
Create Subscription¶
Endpoint: POST /api/v1/subs
Request Body (SubscriptionCreateRequest):
{
"name": "My VPN Subscription",
"description": "Personal VPN configs",
"sources": [
"vless://uuid@server:port?encryption=none#MyServer",
"https://provider.com/subscription",
{
"data": "https://your-domain.com/sub/another-token",
"is_hidden": true,
"max_depth": 1
}
]
}
| Field | Type | Required | Constraints |
|---|---|---|---|
name |
string | Yes | Non-empty after stripping whitespace; max 64 chars |
description |
string · null | No | Max 64 chars |
sources |
array[string | SourceObject] | No | Max 150 items (MAX_SOURCES_PER_SUBSCRIPTION); default [] |
Each sources item can be a plain string (shorthand for {"data": "<string>"}) or a SourceCreateRequest object — see that page for is_hidden/max_depth details. Duplicate data values (after normalization) are silently deduplicated, keeping the first occurrence; if the whole list becomes empty after deduplication, the request is rejected with a validation error.
Response 201 Created (SubscriptionResponse):
{
"token": "abc123xyz456...",
"name": "My VPN Subscription",
"provider_name": null,
"description": "Personal VPN configs",
"sources": [
{
"id": "a1b2c3d4...",
"source_type": "config",
"data": "vless://uuid@server:port?encryption=none#MyServer",
"order_index": 0,
"is_hidden": false,
"max_depth": 3,
"created_at": "2026-04-27T10:00:00Z",
"updated_at": "2026-04-27T10:00:00Z"
}
],
"sources_count": 15,
"created_at": "2026-04-27T10:00:00Z",
"updated_at": "2026-04-27T10:00:00Z"
}
sources_count is the number of fully resolved configs (i.e. it follows nested internal_token sources), not merely len(sources) — it's computed by actually resolving the subscription, the same way GET /sub/{token} does.
Error Responses:
422: Validation failed (empty name, sources list empty after dedup, invalid config/URL format, etc.)422:too_many_sourcesif the resolved source count would exceed the limit;too_many_subscriptionsif the user is already atMAX_SUBSCRIPTIONS_PER_USER409:duplicate_name— a subscription with this name already exists for the user
List Subscriptions¶
Endpoint: GET /api/v1/subs
Response 200 OK (array of SubscriptionListItem):
[
{
"token": "abc123xyz456...",
"name": "My VPN Subscription",
"provider_name": null,
"description": "Personal VPN configs",
"sources_count": 15,
"created_at": "2026-04-27T10:00:00Z",
"updated_at": "2026-04-27T10:00:00Z"
}
]
SubscriptionListItem has every field of SubscriptionResponse except sources — use Get Subscription Details to retrieve the resolved source list for one subscription.
Get Subscription Details¶
Endpoint: GET /api/v1/subs/{token}
Response 200 OK: SubscriptionResponse, same shape as Create Subscription's response.
Error Responses:
404:subscription_not_found
Update Subscription Metadata¶
Endpoint: PATCH /api/v1/subs/{token}
Request Body (SubscriptionUpdateRequest):
| Field | Type | Required |
|---|---|---|
name |
string · null | No — max 64 chars, non-empty after stripping if provided |
description |
string · null | No — max 64 chars if provided |
Both fields are optional and independent — the endpoint does not require at least one to be non-null; sending {} is accepted and is a no-op.
Response 200 OK: SubscriptionResponse
Error Responses:
404:subscription_not_found409:duplicate_nameif the new name collides with another of the user's subscriptions422: Validation failed
Delete Subscription¶
Endpoint: DELETE /api/v1/subs/{token}
Response 204 No Content
Error Responses:
404:subscription_not_found
Add Sources¶
Endpoint: POST /api/v1/subs/{token}/sources
Request Body (SourcesAddRequest):
{
"sources": [
"vless://uuid@server:port#NewServer",
{ "data": "https://new-provider.com/sub", "is_hidden": true }
]
}
| Field | Type | Required | Constraints |
|---|---|---|---|
sources |
array[string | SourceObject] | Yes | 1-150 items after deduplication |
Response 200 OK: SubscriptionResponse with the updated source list.
Error Responses:
404:subscription_not_found422: Empty after deduplication, invalid source,too_many_sources
Replace All Sources¶
Endpoint: PUT /api/v1/subs/{token}/sources
Request Body (SourcesReplaceRequest):
Unlike Add Sources, an empty array is accepted here (sources: []), clearing all sources from the subscription.
Response 200 OK: SubscriptionResponse
Error Responses:
404:subscription_not_found422: Invalid source,too_many_sources
Remove Sources¶
Endpoint: DELETE /api/v1/subs/{token}/sources
Request Body (SourcesRemoveRequest):
| Field | Type | Required | Constraints |
|---|---|---|---|
source_ids |
array[string] | Yes | At least 1 item; each a 32-char hex source hash |
Response 200 OK: SubscriptionResponse
Error Responses:
404:subscription_not_found,source_not_found422: Empty list
Update Config¶
Partially updates a single source's comment, hidden state, and/or nesting depth. Only fields explicitly present in the request body are changed; omitted (or null) fields are left untouched.
Endpoint: PATCH /api/v1/subs/{token}/config
Request Body (SourceUpdateRequest):
{
"config_id": "a1b2c3d4e5f6...",
"comment": "My custom comment",
"is_hidden": false,
"max_depth": 2
}
| Field | Type | Required | Constraints |
|---|---|---|---|
config_id |
string | Yes | 32-char hex source hash. Aliased: config_hash is also accepted as the field name |
comment |
string · null | No | Max 255 chars |
is_hidden |
boolean · null | No | — |
max_depth |
integer · null | No | 0-3 |
Unlike the deprecated Update Config Comment, passing comment: null here leaves the existing comment unchanged — it does not clear it or fall back to a default.
Response 204 No Content
Error Responses:
404:subscription_not_found,source_not_found422: Invalidconfig_idor field values
Update Config Comment Deprecated¶
Deprecated: Superseded by Update Config, which supports the same comment update plus
is_hiddenandmax_depth. Still fully functional.
Endpoint: PATCH /api/v1/subs/{token}/comments
Request Body (CommentUpdateRequest):
| Field | Type | Required | Constraints |
|---|---|---|---|
config_id |
string | Yes | 32-char hex source hash (alias: config_hash) |
comment |
string · null | No | Max 255 chars |
Behavioral difference from Update Config: if comment is omitted or null, this endpoint sets the comment to the server's configured domain name (settings.domain) rather than leaving it unchanged.
Response 204 No Content
Error Responses: Same as Update Config.
Refresh Subscription¶
Force an immediate refresh of all external_url sources in a subscription, bypassing the normal lazy-refresh cooldown.
Endpoint: POST /api/v1/subs/{token}/refresh
Response 200 OK (RefreshSubscriptionResponse):
{
"refreshed": 5,
"failed": 1,
"skipped": 2,
"total": 8,
"message": "Refresh completed with some failures",
"errors": ["https://dead-provider.com/sub: Connection timeout"]
}
total is a computed field (refreshed + failed + skipped), not independently settable or stored. config/internal_token sources are always counted under skipped, since only external_url sources are ever refreshed by this endpoint.
Error Responses:
404:subscription_not_found