Skip to content

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 .conf before 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.

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-url wins.

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.

s-reconnect-on-update: true

Body alternative:

#s-reconnect-on-update: false

URL alternative:

https://panel.example.com/subscription?s-reconnect-on-update=true

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.