Subscription metadata¶
The subscription endpoint response is the body (server list) plus metadata (branding, expiry, updates). Metadata can be delivered two ways, and the client reads both.
A. HTTP response header
Profile-Title: My VPN
B. Comment line in the body
#profile-title: My VPN
★ The HTTP header wins. A #key: value line in the body is used only if there's no header of the same name. This lets you deliver metadata when you can't set headers (static hosting, Telegram files, etc.).
⇄ On/off values. Any header that switches a feature on or off accepts a family of equivalents, case-insensitive: 1 / on / true / yes all mean enabled, and 0 / off / false / no all mean disabled.
⚠ Bypass features: your setting wins over the user's. For all four bypass headers — s-fragment, s-dns, s-noise, s-resolve — the value you send takes precedence over the user's own toggle, off included. The user's toggle only applies when you send nothing at all. This is deliberate: these parameters have to match what your server expects, and a user switching them on their own would simply break the connection.
Live subscription updates¶
A provider can change a subscription's bypass settings and routing without waiting for the next subscription re-fetch: changes made in the provider account reach online devices within minutes and are applied without reconnecting — new values take effect on the next connect. The same rules apply as for the HTTP headers above — in particular, they are honored only while the provider is active. The delivery mechanism is internal to the app: nothing needs to be implemented on the subscription endpoint.
Body line syntax¶
-
Starts with # , then a key [A-Za-z0-9-]+ , then the value.
-
The colon is optional — #profile-title My VPN also works.
-
The key is case-insensitive.
-
The first occurrence of a key wins; duplicates are ignored.
-
The body is scanned both as-is and after full base64 decoding — it works for plain text and for a single base64 blob (the usual vless-link format).
-
A body that starts with
[Interface]is treated as a WireGuard / AmneziaWG.confbefore any JSON or base64 detection (Android 449, iOS / macOS / tvOS 331, desktop 218 and later). -
Only the keys in the table below are read from the body; lines with other keys are ignored.
base64 for non-ASCII (e.g. Cyrillic)
Text fields can be encoded by prefixing base64: — works in both header and body. Applies to profile-title, s-title, announce, sub-expire-button-text.
#profile-title: base64:TXkgVlBO → “My VPN”
Keys¶
Each key works both as a header Key: value and as a body line #key: value. All free.
Branding & links¶
| Key (header / #body) | Purpose |
|---|---|
profile-title / s-title🔒 |
Subscription title (text or base64:) Without profile-title/s-title the subscription is shown under “My servers” as plain servers, without a provider header (Android 469+, Apple 2.0.17 (344)+, Windows/Linux 238+). |
s-sitename🔒 |
Provider/site name |
s-siteurl🔒 |
Provider website URL |
s-tgbot / x-tgbot🔒 |
Telegram bot/channel link |
support-url🔒 |
Support link |
announce🔒 |
Announcement banner text (text or base64:), up to 5 lines — longer text is ellipsized |
announce-url🔒 |
Makes the announcement clickable — opens this URL |
support-email🔒 |
Support email — adds an “Email support” button to the subscription card |
profile-web-page-url🔒 |
Provider web page |
Subscription status & renewal¶
| Key (header / #body) | Purpose |
|---|---|
subscription-userinfo🔒 |
upload=…; download=…; total=…; expire=… (quota + expiry epoch) |
sub-expire / s-showexpire🔒 |
Show expiry status |
sub-expire-button-text🔒 |
Renewal button label (text or base64:) |
sub-expire-button-link-site🔒 |
Renewal button → website |
sub-expire-button-link-tg🔒 |
Renewal button → Telegram |
notification-subs-expire🔒 |
1 — send “subscription expires in N days” notifications, starting 3 days before expiry. Independent of sub-expire / s-showexpire (does not control the renewal buttons and does not depend on them). Absent = no notifications. Also accepted as #notification-subs-expire: 1 in the body. |
Updates & data¶
| Key (header / #body) | Purpose |
|---|---|
profile-update-interval🔒 |
Subscription auto-update interval (hours) |
fallback-url🔒 |
Fallback subscription URL |
geoipurl / geositeurl🔒 |
GeoIP / GeoSite database sources (section 07) |
sort-order🔒 |
Order of servers in the list: ping (fastest first, by measured latency — unmeasured go last), name (alphabetical) or none (as sent, the default). Unknown values are treated as none. |
Migration & access¶
| Key (header / #body) | Purpose |
|---|---|
new-url🔒 |
Fully replace the subscription URL (migration) — see below |
new-domain🔒 |
Replace only the URL host: host or host:port (safe for personal links). Can also be driven from the provider portal without touching your panel — see section 05b |
fallback-domains🔒 |
Backup hosts, comma-separated. If the subscription host stops answering, the app tries these in order, keeping each user's own path and token. Unlike new-domain this changes nothing permanently — a way out when your main address is blocked, not a migration. Set it from the provider portal or send it yourself (section 05b) |
hide-settings🔒 |
Hides the subscription URL and its config from the user — full details in section 04b. 1 hides, 0 un-hides; an absent header changes nothing. |
Identification¶
| Key (header / #body) | Purpose |
|---|---|
providerid / s-providerid🔒 |
Provider ID (section 05). The s- variant is preferred; also read from the URL ?providerid= |
subid🔒 |
Stable subscription ID — lets you change the subscription URL without your users ending up with duplicates (section 05a) |
Bypass¶
| Key (header / #body) | Purpose |
|---|---|
s-noise🔒 |
Noise packets before the handshake — junk traffic sent ahead of the connection so DPI can't recognise its start. Works alongside s-fragment (they share the same egress). on / 1 uses the defaults (rand, packet 50-100, delay 10-20); off / 0 disables. Full form: type=rand;packet=50-100;delay=10-20, or positional rand,50-100,10-20; type is rand, str or hex. → full details in section 08 |
s-resolve🔒 |
Pre-resolve the server address over DoH before the tunnel comes up — for networks where the local DNS forges the answer for your node’s domain. on / 1, off / 0, or a resolver URL with an optional bootstrap IP. → full details in section 08 |
s-reconnect-on-update |
Reconnect the active VPN when the connected server changes after a subscription update. Enabled by default; header overrides the local switch; no Provider ID required. |
s-reality-disable-pq |
Disable REALITY ML-KEM without changing the fingerprint; HTTP/file/URL; no active Provider ID. See Censorship bypass. |
s-fragment🔒 |
TLS ClientHello fragmentation against SNI-based DPI (full reference — section 08). |
s-dns🔒 |
DNS-over-HTTPS inside the tunnel: on / 1 — the built-in default resolver (doh.ffmuc.net); a URL — that resolver; off / 0 — disable. → full details in section 08 |
🔒 Locked keys work only with an active provider — see the full list.
Per-app proxy (Android) — per-app-proxy-*¶
Drive the user's "Proxy for apps" screen from the subscription — the same keys Happ uses, so one panel serves both apps. Android only: iOS/macOS/tvOS and desktop have no per-app routing and ignore these keys.
| Key (header / #body) | Purpose |
|---|---|
per-app-proxy-enable🔒 |
1 / 0 — turn the feature on or off. 1 without a mode keeps the user's current mode (bypass if it was off). |
per-app-proxy-mode🔒 |
off · on — only the listed apps go through the tunnel · bypass — the listed apps go around the tunnel, everything else through it. |
per-app-proxy-list🔒 |
Package names, comma-separated, added to the user's selection. Apps not installed yet are kept and picked up on the first connection after they appear. |
per-app-proxy-list-set🔒 |
Same format, but replaces the selection entirely. |
per-app-proxy-list-invert🔒 |
Same format; selects every installed app except the listed ones. |
Lists accept , ; or whitespace as separators and the base64: prefix. A given set of values is applied once: the app remembers what it applied, so users can still adjust the selection by hand and a routine refresh won't undo them — send different values to push a new selection. Changes take effect on the next connection (Android fixes the app set when the tunnel starts).
per-app-proxy-enable: 1
per-app-proxy-mode: bypass
per-app-proxy-list: ru.sberbankmobile,ru.alfabank.mobile.android,com.idamob.tinkoff.android,ru.yandex.yandexmaps
Provider banner — banner-*¶
A banner on the subscription card, shown only while the provider id is active (INCY-compatible). Colors must be #RRGGBB — anything else is ignored and the app's own theme color is used. The button caption color is picked automatically from the button's brightness, so it stays readable on light and dark backgrounds alike.
| Header | Value & purpose |
|---|---|
banner-text🔒 |
text or base64: — banner body, up to 5 lines |
banner-button-text🔒 |
text or base64:, ≤ 25 characters — button caption |
banner-button-url🔒 |
a URL or a deep link — where the button leads |
banner-bg-color🔒 |
#RRGGBB — banner background |
banner-button-color🔒 |
#RRGGBB — button background |
Bypass parameters at a glance¶
A summary of the four bypass headers — the detailed reference for each is in section 08.
| Header | Values |
|---|---|
s-reality-disable-pq |
true / 1 / on or false / 0 / off; absent inherits the local toggle (off). |
s-fragment |
on | off | packets=<tlshello\|1-3\|all>;length=<min-max>;interval=<min-max>[;maxsplit=<n>] · default when only on is sent: packets=tlshello;length=50-100;interval=10-20 |
s-noise (s-noises) |
on | off | type=<rand\|str\|hex>;packet=<value>;delay=<min-max> · default when only on is sent: type=rand;packet=50-100;delay=10-20 |
s-resolve |
on | off | <doh-url>[;ip=<bootstrap>][, …] · default when only on is sent: the built-in resolver |
s-dns |
on | off | <doh-url> · default when only on is sent: the built-in resolver (doh.ffmuc.net) |
Compatibility with INCY / Happ header names¶
If you already configure another client, the app also understands their atomic keys and folds them into the equivalent setting. Our s-* header always wins — the keys below are read only when the matching s-* header is absent. *-enable: 0 means explicitly off.
| Their keys | Mapped to |
|---|---|
fragmentation-enable · fragmentation-packets · fragmentation-length · fragmentation-interval · fragmentation-maxsplit |
s-fragment |
noises-enable · noises-type / noises-packet-type · noises-packet · noises-delay · noises-rand |
s-noise |
server-address-resolve-enable · server-address-resolve-dns-domain · server-address-resolve-dns-ip |
s-resolve |
noises-rand: <n> becomes type=rand;packet=<n>. noises-rand-range is not supported.
Where a value can come from, and what wins: 1) the HTTP response header — highest priority; 2) a #key: value line in the body (and, for routing commands, a bare *://routing/… line) — used when the header is absent; 3) the app's own setting — when the provider sends nothing. For the four bypass headers the provider's value wins over the user's toggle, off included.
Text values accept plain UTF-8 or a base64: prefix; any header value may also arrive crypt1/-encrypted.
URL migration: new-url / new-domain¶
Re-point the saved subscription URL — the provider can move without the user having to re-add the subscription.
-
new-url— replaces the whole URL (any format), the same for all subscriptions. -
new-domain— changes only the host (scheme, port, path, query are kept); each subscription keeps its own path/token — safe for personal URLs. If both are sent,new-urlwins.
Applies only if the same response contains providerid AND that provider account is active. All local subscriptions with that providerId are migrated. The locked flag is preserved: a subscription from crypt1/addhw stays locked on the new URL.
Compatibility aliases (Happ / INCY)¶
Headers other clients use are read as their SMProxy counterparts when the SMProxy header is absent — a panel written for Happ or INCY works without changes. Aliases are accepted both in HTTP headers and in the #key: value body form.
| Other client sends | Read as |
|---|---|
subscription-name |
profile-title — INCY fallback name |
content-disposition: attachment; filename="x.txt" |
profile-title — filename without .txt / .yaml / .yml; lowest priority |
homepage |
profile-web-page-url — INCY |
hide-url |
hide-settings — 1/true hides, 0/false un-hides |
sub-info-text |
banner-text — Happ “advanced announcements”; 0 = no banner |
sub-info-button-text / sub-info-button-link |
banner-button-text / banner-button-url |
sub-info-color (red · blue · green) |
banner-bg-color — mapped to a hex colour |
sub-expire-button-link |
sub-expire-button-link-tg for t.me / tg://, otherwise sub-expire-button-link-site — Happ single-link form |
routing-enable: 0 |
routing: routing/off — Happ “disable routing” |
subscription-userinfo: 0 |
header absent — INCY “hide the traffic block” |
expire=<milliseconds> in subscription-userinfo |
seconds — values above 32 000 000 000 are treated as ms |
The SMProxy header always wins when both are present.
Reconnect after an update — s-reconnect-on-update¶
Available on Apple 3.0.2 (385), Android 3.0.1 (514), Windows/Linux 2.0.2 (267). No active Provider ID is required. The local setting is enabled by default. An explicit valid header overrides the local setting for that subscription; absent or invalid values use the local setting.
Accepted values: true, false, 1, 0, on, off (case-insensitive; quoted boolean strings are accepted). Encrypted crypt1/ header values are supported. An HTTP response header takes precedence over a body comment and the subscription URL query parameter.
Body alternative:
URL alternative:
After saving a successful refresh, only an active connection using the changed server is restarted. Match first uses a unique unchanged configuration, then an unambiguous name and protocol in the same subscription. Keep names unique and stable when changing server endpoints. Removed or ambiguous entries do not trigger automatic server selection. Names, descriptions and JSON object key order do not trigger a restart. Changing other connection parameters may trigger it. The setting and subscription override are included in portable backups.