DEVELOPER DOCS

Provider documentation

How SMProxy App works with subscriptions: deep-links, response headers, Provider ID, messages, GeoIP/GeoSite and the provider portal. This reference reflects current app behaviour.

Everything is free right now — for the whole open-testing period
01

Overview

SMProxy App is a free cross-platform client for Vless/Reality, VMess, Trojan, Shadowsocks, Socks, WireGuard and AmneziaWG, built on the Xray core. The app does not provide servers and is not a VPN service — the user imports a subscription (config) from a provider and connects. Available on iOS, iPadOS, macOS, Apple TV, Android, Android TV, Windows and Linux.

It's a universal client: it imports a config and connects. Everything else — branding, routing, expiry, statistics — is determined by what the provider returns in the subscription response and by the provider account settings.

📘
Reading these docs with an AI assistant

This documentation is published as a machine-readable source, so Claude Code, Cursor, Codex and other MCP clients can read the real header formats and deep links instead of guessing them.

claude mcp add --transport http smproxy-docs https://gitmcp.io/SMProxy/docs
Setup for other clients →
↑ Back to contents
02

Adding a subscription

Three import methods:

🔗
Subscription link
Paste an https://… link — the server returns the config body and headers.
📷
QR code
Scan a QR encoding a URL or an smproxy:// deep-link.
Deep-link
An smproxy://… link opened from a browser/message or pasted into the add form — resolved on all platforms.

The subscription body can be a list of vless://…, base64links, a base64 blob or JSON configs. All routing, branding and expiry arrive in the response headers (section 04).

↑ Back to contents
03a

Request headers (client → server)

Every subscription fetch carries these headers, so a panel can identify the device and bind a subscription to it (a routing-profile download carries only the User-Agent; geo-database downloads carry none):

Header
Value
X-Hwid
Stable device id. Computed once on first launch and never changes afterwards — not on app updates and not on reinstall. Shown in the app’s About screen; it is what addhw links compare against. Each client app has its own HWID scheme, so a device bound in another client is a different HWID for the panel.
X-Device-Os
Android · iOS · macOS · tvOS · Windows · Linux
X-Ver-Os
OS version string — e.g. 16, 18.6, 10.0.26200
X-Device-Model
Device model — e.g. SM-S942B, iPhone16,2, PC
X-Device-Locale
ru_RU etc. — device language and region
X-App-Version
2.0.5 (422) — marketing version + build in one string
X-App-Build
422 — build number alone — the value to gate on (Android 423+, Apple 313+, desktop 199+; older builds send only X-App-Version and the User-Agent)
User-Agent
SMProxy/<version>/<build>-<OS> — e.g. SMProxy/2.0.5/422-Android. Use it when you want to serve different content to different builds

All header names are case-insensitive.

User-Agent — one format on every platform

SMProxy/<marketing version>/<build>-<OS>
Platform
Example
Android
SMProxy/2.0.5/422-Android
iOS / iPadOS
SMProxy/2.0.9/312-iOS
macOS
SMProxy/2.0.9/312-macOS
tvOS
SMProxy/2.0.9/312-tvOS
Windows
SMProxy/2.0.2/198-Windows
Linux
SMProxy/2.0.2/198-Linux

It is sent on every subscription fetch and whenever a routing profile is downloaded by URL. The build number is what you gate on — the third segment grows on every release and never repeats, while the marketing version can stay the same across several builds. A panel that serves different content to different builds should parse it like this:

^SMProxy/[^/]+/(\d+)-(Android|iOS|macOS|tvOS|Windows|Linux)$
↑ Back to contents
04

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.

AHTTP response header
Profile-Title: My VPN
BComment 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).
  • 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:)
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-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 (dnsforge.de); a URL — that resolver; off / 0 — disable. → full details in section 08
🔒 Locked keys work only with an active providersee the full list.

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-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 (dnsforge.de)

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 has provider_active == true. 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.

↑ Back to contents
04a

Server description

A per-server caption shown under the server name instead of the default protocol label (VLESS | TCP | Reality). Use it to say something the label can't — Netflix / streaming, for gaming, 10 Gbps. Unlike the keys above it is not sent per subscription: it rides with each individual server.

🔒 Honored only while the provider id is active — the same gating as s-fragment and s-dns. Without an active provider id the app falls back to the protocol label.
Rule
Value
serverDescription
Field name
base64
Encoding in links (plain text in JSON configs)

In share links

Inside the #fragment, after the title, separated by ?:

vless://uuid@host:443?security=reality&type=tcp#🇳🇱 Netherlands?serverDescription=TmV0ZmxpeCAvIHN0cmVhbWluZw==

The same syntax works for vless://, trojan://, ss:// and socks://.

In vmess links

A field inside the base64-encoded JSON body:

{ "ps": "🇳🇱 Netherlands", "add": "host", "port": "443", "serverDescription": "TmV0ZmxpeCAvIHN0cmVhbWluZw==" }

In JSON configs

Inside a meta object next to remarks — here the value is plain text, not base64:

{ "remarks": "🇳🇱 Netherlands", "meta": { "serverDescription": "Netflix / streaming" }, "outbounds": [ … ] }

In links, a value that isn't valid base64, or decodes to nothing, is ignored — the server keeps its protocol label rather than showing broken text.

↑ Back to contents
04b

Hiding the subscription URL — hide-settings

hide-settings: 1 stops the subscription URL and its raw content from leaving the device. It puts the subscription into exactly the same state as an encrypted (crypt1) or hardware-bound (addhw) one — internally the app treats “locked” and “hidden by the provider” as one condition.

What it blocks

Blocked
Detail
The URL field in the subscription editor
The field is not rendered at all — the user cannot read, copy or change the address
“Copy link” in the subscription menu
Removed
“View config”
Removed, and the config-export routine itself refuses to return anything even if called from elsewhere in the app
Editing the URL through an update
An edit that would change the address is ignored; the name and the routing profile stay editable

What keeps working

Connecting, ping, traffic statistics, subscription refresh, the server list, choosing servers, routing profiles, the expiry UI — the user simply cannot see or export the address.

Values and behaviour

Value
Effect
1
Hide. Also clears any pending “unlock” request from the user.
0
Un-hide — and also lifts a crypt1 lock on that subscription, if the provider set one. This is deliberate: the provider owns the link, so an explicit 0 from an active provider opens the subscription completely.
absent
Changes nothing — the current state is kept. The flag is sticky: set it once and it holds across refreshes until you send an explicit 0.

Anything other than 1 or 0 is ignored and logged as “value not recognised” — on / off / true / false are not accepted here.

Accepted as an HTTP response header, as #hide-settings: 1 in the body, and from the provider account. Honored only while the provider id is active — an inactive or unknown provider cannot hide (or reveal) anything.

↑ Back to contents
04c

Two channels, one rule: who set a value can clear it

The same keys can arrive two ways — in the subscription response (an HTTP header or a #key: line) and from the provider account (applied to online devices within minutes). They follow one rule:

Situation
Result
The key is in the subscription response
Its value wins and the field is marked “set by the subscription”
The key is in the provider account
Its value wins over the subscription’s and the field is marked “set by the account”
The key is missing from the account update, and the field was set by the account
The field is cleared — you removed it in the panel, so it disappears in the app
The key is missing from the account update, and the field was set by the subscription
The field is kept — the account never clears what the subscription set

In short: whoever wrote last, wins, and a value can only be removed by the side that put it there. That is how you take an announcement or a banner down — clear it in the panel and it disappears on the next update; the subscription response keeps whatever it sends.

Fields with no “empty” state — the subscription name, brand links, hide-settings — are never auto-cleared; send a new value to change them.

⚠️ This applies to all keys, including bypass (s-fragment, s-noise, s-resolve, s-dns) and routing: if you set s-fragment from the panel and later remove it there, fragmentation switches off on the next update.

↑ Back to contents
05

Provider ID

Provider ID links a subscription to a provider account so the provider can see usage statistics for their subscription. It restricts nothing — the app works fully without it. Free.

How to send it (any method)
1URL query — …?providerid=<ProviderID>
2Comment in the subscription body — line #providerid <ProviderID>
3HTTP response header — providerid: or s-providerid:

If several are provided, the HTTP header takes priority. You get your Provider ID (and the option to set a custom one) in the provider portal — see section 08.

Without a Provider ID the subscription still works fully; the provider just gets no statistics. What does depend on an active Provider ID is listed below — the same tables are repeated at the end of this page for quick reference.

🔒Requires an active Provider ID

Area
Keys
Bypass
s-fragment s-noise s-noises s-resolve s-dns — and their INCY/Happ equivalents fragmentation-* noises-* server-address-resolve-*
Migration & fallbacks
new-url new-domain fallback-url fallback-domains
Provider branding
s-siteurl s-sitename s-tgbot x-tgbot sort-order serverDescription
Banner & contacts
banner-text banner-button-text banner-button-url banner-bg-color banner-button-color announce-url support-email
Expiry UI
sub-expire s-showexpire sub-expire-button-text sub-expire-button-link-site sub-expire-button-link-tg notification-subs-expire
Access
hide-settings

Works with or without a Provider ID

Key
Note
profile-title / s-title
Subscription name
profile-update-interval
Auto-refresh interval
profile-web-page-url
Provider web page
announce
The announcement text itself — but announce-url (making it clickable) needs an active provider
support-url
Support link
subscription-userinfo
Quota and expiry date
subid
Stable subscription id
routing autorouting routing-update-url routing-update-interval routing-profile
Routing profiles, including the deep-link command forms — a profile carries rules only, so it needs no active provider
geoipurl / geositeurl
Geo database addresses — subscription response only

The same gating applies however the value arrives — an HTTP header, a #key: line in the body, or the provider account.

↑ Back to contents
05a

Domain verification — statistics without a Provider ID

Statistics only work when the app knows your Provider ID. If a build does not send it — an older release, a client you do not control, a config imported by hand — those check-ins land nowhere and you lose sight of your own users.

Verify the domain your subscriptions are served from, and check-ins without a Provider ID are attributed to you automatically, based on the subscription URL the app reports.

How to verify
1Open provider.smproxy.io/provider/domains and add your domain. You can paste a full subscription URL — the domain is extracted for you.
2Add the TXT record shown on the page.
3Press Check. DNS does not propagate instantly — if the first attempt fails, wait a couple of minutes and try again.
Name: smproxy-verification.your-domain.com Value: 8f3a91c24b774c1e9a025e6d1f0b7c33
☁️ Cloudflare shortcut. If your DNS is on Cloudflare, press Add via Cloudflare instead. You are taken to Cloudflare's consent screen, we create the TXT record for you, and the domain is verified straight away — no copying required. We ask for DNS access only, and the record stays in place afterwards.

What it covers

Subdomains are included. Verifying example.com covers sub.example.com, eu.sub.example.com and any other depth — you do not need to add them separately.

Matching is done on the registrable domain, so multi-level suffixes behave correctly: example.co.uk is a domain you can own, co.uk is not, and evil-example.com never matches example.com.

A domain belongs to one provider. If someone else has already verified it, adding it again is refused.

Priority — Provider ID wins

Check-in contains
Attributed to
A Provider ID that exists
That provider — the domain is not consulted
No Provider ID, subscription URL on a verified domain
The domain's owner
A Provider ID we do not know, URL on a verified domain
The domain's owner
Neither
Nobody; the check-in is marked unattributed

Domain verification never overrides an explicit Provider ID — it only fills the gap when one is missing or unusable.

Where the domain shows up

  • Statistics — a Subscription domains breakdown showing how many devices use each host, and the host under each device's subid. Full hosts are shown, so you can tell sub.example.com from sub3.example.com.
  • Messages — you can target a message at a domain: *.example.com for the domain and all its subdomains, or a single host for just its users.

Only the host is ever displayed — subscription tokens in the URL are never shown.

↑ Back to contents
05b

Moving users to another domain

Your subscription domain gets blocked, or you simply move to a new one. Both cases are handled from the portal — you do not have to change anything in your panel, and users do not have to re-add their subscription.

Both rules are keyed on the domain the request arrived at, so you can run several at once — one per subdomain — and remove them individually.

Migration — new-domain

Open provider.smproxy.io/provider/domains, press Move next to a verified domain and pick:

  • From*.example.com to move subscriptions from the whole domain including its subdomains, or a single host such as sub.example.com to move only that one.
  • To — the new host. It has to be verified in the same account, so nobody can point your users at a domain they do not own.

From then on, every subscription request that arrives at the old host gets a new-domain header, and the app rewrites the address itself. Each user keeps their own path and token:

old.example.com/sub/abc123 → new.example.com/sub/abc123
Chains are refused. If b.example.com already has a migration of its own, you cannot make it the destination of another one — users would be handed an address that immediately moves again.

Backup hosts — fallback-domains

Press Backups next to a domain and list the hosts to fall back to, one per line. Every entry must be a verified domain of yours. The list is sent as fallback-domains for requests to that domain, and the app walks it in order if the main host stops answering.

Use a migration when you have actually moved; use backups when the main address may become unreachable and you want the app to find its own way through.

Two delivery channels

Both rules reach the app in two independent ways:

  • Subscription response headersnew-domain / fallback-domains, as described above. Requires the request to still reach one of your domains.
  • The app's own service channel — the app also picks up the new-domain / fallback-domains you configured for your domain over its own service connection, without fetching the subscription. They are applied on the next subscription refresh: new-domain re-points the stored URL permanently, backup hosts are saved and tried whenever the main host stops answering.

The second channel matters when your subscription domain is already unreachable: it runs over the app's own service endpoints, so existing users still receive the migration or the backup list even though they cannot fetch the subscription itself.

Requires an app build from late August 2026 or newer; older builds keep using headers only.

↑ Back to contents
05c

Subscription ID — subid

What it solves. When you hand a user a new subscription URL — a domain migration, a re-issued token, a move to another panel — the app has no way to tell it is the same subscription. It adds a second entry and the user ends up with two copies of the same thing.

Send a subid and that stops happening: the app treats a matching subid as the same subscription, replaces its URL with the new one and refreshes its servers in place.

Where it can come from (any one — same rules as the other metadata)
1HTTP header — subid: my-sub-42
2Body comment — a line #subid: my-sub-42 in the subscription body
3URL query — …?subid=my-sub-42

An HTTP header wins over a body comment, which wins over the URL. Format: letters, digits, -, _ and spaces ([A-Za-z0-9 _-]+). Anything else is ignored — the value is simply not stored.

Situation
What the app does
Same URL already added
Updates that subscription (unchanged behaviour)
New URL, subid matches an existing subscription
Updates that one: new URL + fresh servers, keeps the user's custom name
New URL, subid is new or absent
Adds a new subscription (unchanged behaviour)

subid is optional. Without it everything works exactly as before — duplicates are detected by URL only. Sending it is simply a way to keep control of your users' entries when your addresses change.

Pick a value that never changes for a given user's subscription. It doesn't have to be secret — it is only compared with what the app already has — but it must be stable and unique per subscription. A UUID or your internal subscription id are both fine.

Example
subid: 8f3a91c2-4b77-4c1e-9a02-5e6d1f0b7c33

Available on all platforms: iOS, Android, macOS, Windows, Linux and Apple TV.

↑ Back to contents
06

Messages & notifications

SMProxy exclusive

A provider can send users in-app messages. They arrive inside the Provider ID check response and land in the app inbox (bell icon + unread counter), optionally triggering a system notification.

Each message has: id, title, body, optionally date, type (info/update/promo/warning), url, notify and targeting. Read status is stored locally per device.

Targeting

Compose messages in the provider portal. By default a message goes to everyone; each filter below narrows that down, and they combine.

Platform and app build

Tick the platforms that should receive the message. Leave the build field empty to reach every version of that platform, or set a condition (<, , =, , >) to reach only some of them.

Build numbers are per-platform — iOS 200 and Android 200 are unrelated releases — so a condition is always tied to a platform. Ticking nothing means “all platforms”; as soon as you tick one, the others receive nothing. That is how you reach users still on an old iOS build without touching Android:

Ticked
Build condition · Who gets it
nothing
— · everyone
iOS
— · every iOS user
iOS
< 200 · iOS users below build 200
iOS, Android
iOS < 200, Android < 300 · old builds on both, nothing elsewhere

Subscription domain

Pick a verified domain to reach only the users whose subscription is served from it — *.example.com for the domain and all its subdomains, or a single host such as sub.example.com for that host alone. The portal shows how many devices are currently on each host.

Requires a verified domain — see Domain verification (section 05a).

A single device

Paste a HWID to send the message to exactly one device — useful when you are walking one user through a problem.

↑ Back to contents
07

GeoIP / GeoSite

The Geo rules the tunnel uses live inside the imported Xray config and are resolved against the geoip.dat / geosite.dat databases. The app ships with built-in databases and can download updated ones from the geoipurl / geositeurl headers (supported on iOS, Android and desktop OSes); a freshly downloaded database replaces the built-in one for the engine.

↑ Back to contents
07a

Routing profiles

A routing profile is a full rule set: what goes straight out, what goes through the tunnel and what is blocked, plus optional split-DNS. It answers “my subscription is a single vless:// link, so there is no routing inside it” — the profile carries routing separately from the servers.

Format compatibility. The profile format is the same one Happ uses, field names included. That's deliberate: profiles travel between apps as links and files, and interoperability beats our own naming.

The profile

{ "Name": "Bypass RU", "GlobalProxy": true, "DirectSites": ["geosite:category-ru", "domain:gosuslugi.ru"], "DirectIp": ["geoip:ru", "192.168.0.0/16"], "ProxySites": ["geosite:youtube"], "BlockSites": ["geosite:category-ads-all"], "RemoteDNSType": "DoH", "RemoteDNSDomain": "https://dnsforge.de/dns-query", "DomesticDNSType": "DoU", "DomesticDNSIP": "77.88.8.8", "DnsHosts": { "example.com": "1.2.3.4" }, "DomainStrategy": "IPIfNonMatch", "UpdateUrl": "https://example.com/routing.json", "UpdateInterval": 24, "LastUpdated": 1756600000 }
Field
Meaning
Name
Profile name. It is also the update key: importing a profile with the same name replaces the old one instead of creating a duplicate.
GlobalProxy
Where unmatched traffic goes: true — through the tunnel, false — direct.
DirectSites / ProxySites / BlockSites
Domains and categories: geosite:ru, domain:example.com, regexp:…
DirectIp / ProxyIp / BlockIp
IPs, subnets and geoip:ru.
RemoteDNS* / DomesticDNS*
Split-DNS: the remote resolver answers for proxied names, the domestic one for direct names. Six fields — see “Split-DNS fields” below.
DnsHosts
Static domain → IP mappings.
DomainStrategy
Rule matching order: AsIs (default), IPIfNonMatch, IPOnDemand.
FakeDNS
Accepted for compatibility (true/false, boolean or string as in Happ); the SMProxy core does not use FakeDNS, the value is ignored.
UpdateUrl
Where to fetch a fresh copy of this profile. With it the profile is self-contained: hand it out as base64 or a file and it still keeps itself up to date.
UpdateInterval
How often to refresh, in hours. 12…168 (a week); 24 by default.
LastUpdated
Optional. Unix timestamp of the profile’s last change — number or numeric string, as in Happ. Shown in the profile card and used as a version guard — see below.

Rules are applied in the order block → direct → proxy, and they sit ahead of any rules that came with the server config.

GlobalProxy and FakeDNS are accepted both as JSON booleans and as the strings "true" / "false" that Happ and INCY emit. Fields other clients add for their own UI — RouteOrder, UseChunkFiles — are ignored: SMProxy always applies rules in the order block → direct → proxy.

Split-DNS fields

Field
Meaning
RemoteDNSType
Protocol of the remote resolver: DoH, DoT, DoQ or DoU (plain UDP).
RemoteDNSDomain
Resolver address — a DoH/DoT URL such as https://dnsforge.de/dns-query. Empty for plain UDP.
RemoteDNSIP
Resolver IP. With a RemoteDNSDomain it is the bootstrap address of that domain — the same role ;ip=… plays in the s-resolve header; without a domain it is a plain UDP resolver.
DomesticDNSType / DomesticDNSDomain / DomesticDNSIP
The same three fields for the domestic resolver (direct names).
RemoteDns / DomesticDns
Legacy spellings from older Happ/INCY profiles — an alternative to RemoteDNSIP / DomesticDNSIP. Read only when the new field is absent or empty. Do not use them in new profiles.

How the resolvers are used:

  • Which resolver answers what. Remote — names that go through the tunnel, domestic — names routed direct (DirectSites). The remote resolver is queried through the tunnel, the domestic one over the physical network.
  • RemoteDNSDomain is also the client's own resolver for its service requests (subscription refresh, profile download) when the subscription sends no s-resolve. A routing profile therefore sets DNS for everything, not just for traffic inside the tunnel.
  • Bootstrap. The ;ip=… suffix of s-resolve does not work inside RemoteDNSDomain — there is no parsing there, the address would go out with junk at the end. Put the bootstrap address into RemoteDNSIP.
  • Fallback chain. The profile's resolvers go first, then the DNS servers from the connection config itself (their own domains / skipFallback splits are kept). If every profile resolver is unreachable, resolution falls through to the config's servers.
  • Client support for arrays: SMProxy from Android 411 / iOS, macOS, tvOS 305 / Windows, Linux 193. Happ, INCY and older SMProxy builds read only the string form — send a string unless you know your users are updated.

String or array — an SMProxy extension. RemoteDNSDomain, RemoteDNSIP, DomesticDNSDomain and DomesticDNSIP each accept a single string (the classic form, compatible with Happ/INCY) or an array of strings:

"RemoteDNSDomain": ["https://dns1.example/dns-query", "https://dns2.example/dns-query"], "RemoteDNSIP": ["1.2.3.4", "5.6.7.8"]

Order = priority: the core queries resolvers top-down, so the second entry is the fallback for the first. *DNSIP[i] is the bootstrap address for *DNSDomain[i]; an IP without a paired domain acts as a plain resolver.

Version guard — LastUpdated

When a profile with the same Name arrives automatically (subscription headers, an UpdateUrl refresh, a link), it replaces the stored one only if its LastUpdated is greater. If either side has no timestamp, the profile is replaced unconditionally. Bump the value whenever you change the profile. Manual imports from the app UI always apply.

🧩
Don't write the JSON by hand
The builder assembles a profile from plain fields and gives you the JSON, an smproxy://routing/onadd/… link and its QR code. Everything runs in your browser.
Routing builder →

Handing a profile to the user

By link — the natural way: from a browser, a message or a QR code.

Link
Effect
smproxy://routing/add/<base64|url>
Import the profile, don't switch to it
smproxy://routing/onadd/<base64|url>
Import and activate
smproxy://autorouting/add/<url> · …/onadd/<url>
Same, plus keep it updated from that URL
smproxy://routing/off
Turn routing off

The payload may also be passed as ?data=<base64>. A URL may return the profile JSON, its base64, or another smproxy:// link — all three are accepted.

By subscription header — the profile arrives with the subscription itself.

Header
Purpose
routing
The profile: base64, raw JSON, or a URL to fetch it from. Refreshed together with the subscription.
autorouting
A URL to fetch the profile from and keep updating on its own schedule.
routing-update-url
Where to update the profile from — when you don't want UpdateUrl inside the profile itself.
routing-update-interval
Update interval in hours (12…168). Clamped to that range.
routing-profile
Which profile this subscription should use, by name.

A profile arriving from a subscription is added to the list but not switched on — the choice stays with the user. These keys work with or without a Provider ID.

Headers take deep-link commands too. The value of routing / autorouting may be a deep-link command — the same forms as the clickable links, with any scheme word (smproxy://, happ://, yourapp://): inside a header the word before :// does not matter.

Header value
Effect
*://routing/add/<base64|url>
Import the profile into the list, don’t assign it
*://routing/onadd/<base64|url>
Import and assign to this subscription
*://autorouting/add/<url> · …/onadd/<url>
Same, plus keep the profile updated from that URL
*://routing/off
Detach the profile from this subscription — the provider switches its routing off

The payload may be crypt1/<base64>-encrypted, like any other header value.

The same commands are also accepted as a bare line in the subscription body, right among the server links (INCY-compatible):

vless://uuid@server1:443?security=tls#Server1 vmess://eyJhZGQiOiAic2VydmVyMi... incy://routing/onadd/ewogICJOYW1lIjogIl...

A real HTTP header, or an explicit #routing: body header, takes precedence over a bare line.

Global vs per-subscription

The user picks one active profile in Settings — the global choice. A subscription can override it: if it names a profile (in its own settings, or via the routing-profile header), connecting to that subscription's server uses that profile instead. Both are switchable off: globally in Settings (or with routing/off), and per subscription with “Do not apply”.

Which profile wins

When a subscription refreshes, several of the headers above can arrive at once. They are resolved in this order:

1 routing / autorouting — a profile sent inline wins over one fetched from routing-update-url. Send the profile itself when you want to be sure exactly which version the client gets.
2 routing-update-url alone — if no profile came inline, the client fetches one from this URL and imports it. The URL works on its own; it does not need routing alongside it.
3 routing-profile (a name) — decides which profile the subscription uses, and it outranks whatever arrived in this refresh. Naming Corporate while also sending a profile called Default means the subscription uses Corporate; Default is still imported into the list.

A name that matches nothing changes nothing. If routing-profile: Corporate arrives and no profile by that name exists on the device, the subscription keeps the routing settings it already had. The client will not fall back to some other profile, and it will not clear the setting — a typo in the header can't silently reroute your users' traffic.

The global choice is never touched. Everything above sets the profile for that subscription. The profile the user picked in Settings stays as it is, and keeps applying to every other subscription. To change what a subscription uses, use routing-profile; there is no header that repoints the user's global selection.

Changes apply on the next connect. Profiles are read when the tunnel config is built, so a profile that arrives while connected takes effect after a reconnect — refreshing the subscription mid-session does not reroute live traffic.

Which resolver is used, in order
the s-resolve header → the RemoteDNSDomain field of the active routing profile → the user's own setting → the built-in list (if the user enabled it) → the system resolver.
s-resolve: off disables pre-resolving altogether — the profile is not picked up in that case.
↑ Back to contents
08

Censorship bypass

WHY IT MATTERS

DPI systems often block by SNI (the destination domain) read from the TLS ClientHello — the first packet of every TLS/Reality handshake. Fragmentation splits that ClientHello into many tiny TCP segments with micro-delays, so DPI can't reassemble the SNI → can't see the domain → can't cut by domain. It runs entirely on the stock Xray core (a freedom outbound with fragment settings) — with no extra server or infrastructure.

The four parameters below solve different problems and are often used together.

The app configures everything for you. You do NOT need to add a fragment outbound manually: when enabled, the app inserts it and routes all direct TLS/Reality traffic through it on ANY config, leaving multi-hop chains untouched — only the hop the DPI sees gets fragmented.

08.1 · s-fragment — TLS fragmentation

Enabling — the s-fragment

Value
Effect
absent / off
Off (an absent header leaves the previous value unchanged)
on
On, with default parameters
packets=tlshello;length=50-100;interval=10-20;maxsplit=100-200
On, with these parameters

Sent as the HTTP response header s-fragment: or as a body comment line #s-fragment: packets=tlshello;length=50-100;interval=10-20.

🔒 Requires an active Provider ID. Like any provider-controlled field, s-fragment is honoured only while your Provider ID is active. An inactive or unknown provider can't enable fragmentation on users' devices.

Parameters

Parameter
Format · default · description
packets
tlshello or N-M · default tlshello
Which packets to fragment. tlshello — only the TLS ClientHello (carries the SNI); recommended, minimal overhead. 1-3 — the first 1–3 outbound packets.
length
min-max (bytes) · default 50-100
Fragment size, random within the range. Smaller = harder to reassemble but more overhead.
interval
min-max (ms) · default 10-20
Delay between fragments, random within the range. Jitter hinders buffering and reassembly.
maxsplit
min-max · default unset
Optional (newer cores only). Caps the number of fragments; without it, unlimited. Older cores ignore it.

Defaults suit most networks. Tune for a specific DPI: harder blocking → smaller length and/or larger interval; slower speed → larger length and smaller interval.

Parameter names are case-insensitive (maxsplit and maxSplit are the same key), and the packets key is pluralpackets=, not packet=. packet= belongs to s-noise, where it means the payload.

⚙️Users can enable it themselves

A user can also enable fragmentation and edit the same parameters manually — in the app: Settings → Bypass → TLS fragmentation, independently of the provider. When enabled manually, their parameters take priority over the header.

08.2 · s-noise — noise packets

Junk traffic sent ahead of the connection so DPI can't recognise its start. It works alongside fragmentation — they share the same egress, and together they cover both the start of the connection and what's inside it.

Values: on / 1 uses the defaults (rand, packet 50-100, delay 10-20); off / 0 disables it. Full form is type=rand;packet=50-100;delay=10-20, or positional rand,50-100,10-20; type accepts rand, str or hex.

Both spellings of the header are accepted — s-noise and s-noises.

08.3 · s-resolve — pre-resolving the server address

Resolves the server address over DoH before the tunnel comes up. Use it where the local DNS returns a forged answer for your node's domain: without it the client gets a wrong IP and simply never connects.

Values: on / 1 uses the default resolver (dnsforge.de); off / 0 disables it; a URL sets your own.

Where even the resolver's own domain is poisoned, add a bootstrap IP with ;ip=… — for example https://dnsforge.de/dns-query;ip=49.12.67.122. The original domain stays as SNI, so TLS/Reality still validate. If the resolver doesn't answer within a second and a half the normal lookup is used — the parameter can only help, never block the connection.

Several resolvers can be listed, separated by commas, each with its own bootstrap IP — they are tried in order until one answers. A single address behaves exactly as before: it is the same format without commas.

s-resolve: https://a.example/dns-query;ip=1.2.3.4, https://b.example/dns-query;ip=5.6.7.8

A list beats the app's built-in set: you name the resolvers you know work on your users' networks, which the built-in list cannot know. It also can't be switched on by a header at all — only by the user's own toggle.

08.4 · s-dns — DNS-over-HTTPS in the tunnel

Encrypts ordinary DNS queries inside the tunnel. Values: on / 1 enables the built-in default resolver (dnsforge.de); a URL such as https://dnsforge.de/dns-query sets your own; off / 0 disables it. Resolved DNS travels through the tunnel, not the physical network.

Order of application
Provider parameters always outrank the user's own settings, off included. When the user configures it themselves, the order is: fragmentation → noise → DoH.
↑ Back to contents
09

WireGuard/AmneziaWG

WireGuard and AmneziaWG servers behave like any others: they sit in the same list next to VLESS/Trojan/Shadowsocks and the user picks them the same way. There is no separate mode and no extra setup. It runs on the stock Xray core (a wireguard outbound) — nothing new is required on the provider side beyond the config itself.

Option A — in a JSON subscription

A normal Xray config whose outbound has "protocol": "wireguard":

{ "remarks": "🇳🇱 Netherlands — WG", "outbounds": [{ "tag": "proxy", "protocol": "wireguard", "settings": { "secretKey": "<client private key>", "address": ["10.0.0.2/32"], "mtu": 1420, "peers": [{ "publicKey": "<server public key>", "preSharedKey": "<optional>", "endpoint": "example.com:51820", "allowedIPs": ["0.0.0.0/0", "::/0"], "keepAlive": 25 }] } }] }

The server address shown in the app comes from peers[0].endpoint. WireGuard has no vnext or servers block — the endpoint string is the only place the host lives.

AmneziaWG fields. The same outbound accepts obfuscation parameters — add them next to the usual ones. They are only for AmneziaWG; for plain WireGuard leave them out entirely (any zero value is ignored and the connection behaves exactly as before).

{ "protocol": "wireguard", "settings": { "secretKey": "…", "address": ["10.66.66.17/32"], "mtu": 1420, "jc": 4, "jmin": 40, "jmax": 70, "s1": 15, "s2": 25, "s3": 0, "s4": 0, "h1": 1111111, "h2": 2222222, "h3": 3333333, "h4": 4444444, "peers": [ { "publicKey": "…", "endpoint": "1.2.3.4:51820", "allowedIPs": ["0.0.0.0/0"] } ] } }
Field
Meaning
jc
How many junk packets to send before the handshake (4–12)
jmin / jmax
Junk packet size bounds, bytes (about 8–80)
s1 / s2
Prefix size for the init and response packets (15–150)
s3 / s4
Prefix size for the cookie and transport packets; optional — omit when the server does not set them
h1–h4
Rewritten header types — without them the packets are recognised by signature. A single number or, for AmneziaWG 3.1 servers, a range "a-b" as a string ("h1": "1000-2000"); ranges are applied from Android 428, iOS/macOS/tvOS 2.0.10 (315) and desktop 203 — older builds accept numbers only
s1s4 and h1h4 must be identical on the server and in the client config, otherwise the connection silently fails: the tunnel comes up but no traffic passes. jc, jmin and jmax do not have to match — each side sends its own junk.

Server version — 3.1 and older. The app is built on the AmneziaWG 3.1 branch and stays compatible with every earlier one, so there is nothing to upgrade on your side. Send only the parameters your server actually uses; anything you leave out keeps its default and is ignored.

Added in
Parameters
0.2.11 and earlier
jc, jmin, jmax, s1, s2, h1–h4
0.2.13
s3, s4
0.2.16
i1–i5
3.0
header_protection_key, content_padding_addition, rekey_after_time, rekey_timeout, reject_after_time, keepalive_timeout, max_handshake_attempts
3.1
random_trailers, disable_cookies, ranges a-b for h1–h4 and PersistentKeepalive

A 3.1-only parameter sent to an older server is simply not understood by it, so keep the two sides in step: configure the client with exactly what the server runs.

Option B — a wireguard:// link

wireguard://<base64 of a standard .conf> — the base64 holds an ordinary WireGuard configuration file, exactly the text you hand to the official client. An optional #Name anchor sets the displayed title:

wireguard://W0ludGVyZmFjZV0K…#🇳🇱%20Netherlands

The decoded payload is simply:

[Interface] PrivateKey = <client private key> Address = 10.0.0.2/32 DNS = 1.1.1.1 [Peer] PublicKey = <server public key> PresharedKey = <optional> Endpoint = example.com:51820 AllowedIPs = 0.0.0.0/0, ::/0

This form works everywhere the other share links do: pasting into the add form, inside an encrypted crypt1 link, or as a line in the subscription body.

Build a link without writing code

Paste a .conf into the helper page — it assembles the wireguard:// link and a QR code right in your browser, with no outbound requests.

Open the link builder →

Keys are passed to the core as-is — the app does not re-encode them.

Client-side import

From Android 379, iOS/macOS 2.0.3 (277+) and desktop 169 the add screen takes a WireGuard/AmneziaWG config in four ways — you can hand configs to users in any of them:

1A .conf file — the “Choose config file” button (Android, iOS, macOS, Windows, Linux; Apple TV has no file picker — it adds configs from the phone).
2Raw config text — the [Interface]…[Peer] body pasted straight into the add field, on every platform.
3A link that serves the config — an https address returning the .conf text itself. Many panels hand out configs exactly this way.
4A scheme link or QRwireguard:// / amneziawg:// / awg:// / wg:// carrying base64 of the config, as described above.

AmneziaWG obfuscation parameters (Jc, Jmin, Jmax, S1, S2, H1H4) are picked up from [Interface] in all four. Older builds accept the scheme link and QR only.

C. AmneziaWG — WireGuard with obfuscation

AmneziaWG is the same WireGuard plus obfuscation: junk packets and rewritten header types that keep DPI from recognising a WireGuard handshake.

Link schemes. wireguard://, amneziawg://, awg:// and wg:// are all accepted and parsed the same way — the payload is base64 of a standard .conf, with an optional #Name fragment.

amneziawg://<base64url of the .conf>#Germany

Obfuscation parameters live in [Interface] of that .conf, next to the usual keys:

[Interface] PrivateKey = … Address = 10.66.66.17/32 MTU = 1420 Jc = 4 ; how many junk packets to send before the handshake Jmin = 40 ; junk packet size, lower bound Jmax = 70 ; junk packet size, upper bound S1 = 15 ; init packet prefix size S2 = 25 ; response packet prefix size S3 = 0 ; cookie packet prefix size — optional, omit unless the server sets it S4 = 0 ; transport packet prefix size — same H1 = 1111111 ; rewritten header types — otherwise recognised by signature (3.1 servers may use a range: H1 = 1000-2000) H2 = 2222222 H3 = 3333333 H4 = 4444444 [Peer] PublicKey = … Endpoint = 194.61.120.25:57932 AllowedIPs = 0.0.0.0/0,::/0

Leave them out and you get plain WireGuard — the app and the core behave exactly as before. The same applies field by field: send only what your server runs. Any AmneziaWG version works, 3.1 and older — see the version table in section A.

🛠️
Build the link from a .conf
The generator takes AmneziaWG configs too — obfuscation parameters carry over as-is. Everything runs in your browser.
Link generator →

Several locations in one file. A subscription URL may return a JSON container instead of links:

{ "type": "amneziawg", "version": 1, "servers": [ { "name": "Germany", "config": "<base64url of the .conf>" }, { "name": "Netherlands", "config": "<base64url of the .conf>" } ] }

type is accepted as amneziawg, awg, wireguard or wg. A malformed entry is skipped without taking the rest of the list down with it.

Notes for operators

  • IPv6 endpoints are supported, written the standard way: [2606:4700:d0::a29f:c001]:2408.
  • keepAlive (seconds) — persistent keepalive. Set it if your clients are behind NAT: without it the translation expires and the tunnel goes quiet in one direction.
  • MTU in the config controls what the client sends. What the server sends is set by the MTU of the wg0 interface on the node — configure both, otherwise a path that can't carry full-size packets will pass the handshake and then stall.
  • Routing on the node matters: ip route get <client ip> must resolve via wg0. If it points at the default gateway the handshake still succeeds (the daemon answers it directly) but data never reaches the client. Looks like blocking — it isn't.
↑ Back to contents
10

Link formats

Everything the client accepts, in one place. All schemes are case-insensitive.

Server links

Scheme
Payload
vless:// vmess:// trojan:// ss:// socks://
Standard share links, as issued by any panel
wireguard:// · amneziawg:// · awg:// · wg://
base64 of a plain WireGuard .conf — the same text the official client takes — or base64 of xray-style JSON settings
hysteria2:// · hy2://
Accepted and stored, but not yet supported by xray-core — such a server will not connect

Base64 is read leniently — standard and URL-safe alphabets, with or without padding.

A #fragment sets the display name (percent-encoded). It may also carry a server description — see section 04a.

What “View config” shows

The client applies one simple rule: if the link carries base64, it shows the decoded content; otherwise it shows the link as it is. So wireguard://<base64> displays the readable .conf, while vless://uuid@host:443?… stays a link — there is nothing to decode. JSON payloads are pretty-printed.

Subscriptions

A subscription URL may return:

plain text — one link per line;
base64 of that same list;
JSON — a full xray config, or a container with several WireGuard/AmneziaWG servers:
{"type":"amneziawg","version":1,"servers":[ {"name":"Germany","config":"<base64 of .conf>"}]}

type is accepted as amneziawg, awg, wireguard or wg. A broken entry is skipped rather than failing the whole subscription.

Encrypted header values

Header values can be sent in a closed form, so links and settings don't travel around in the open. Any subscription header may carry its value encrypted, using the same crypt1/ prefix as the deep links:

routing-update-url: crypt1/<base64> routing: crypt1/<base64> autorouting: crypt1/<base64> s-dns: crypt1/<base64> s-fragment: crypt1/<base64> s-resolve: crypt1/<base64> s-noise: crypt1/<base64> new-url: crypt1/<base64> s-siteurl: crypt1/<base64>

The client strips the prefix, decrypts the payload and then treats the result exactly as if it had arrived in the clear. Without the prefix nothing changes — existing subscriptions keep working untouched. Encryption is optional and per-header: a single response can freely mix encrypted and plain values.

The encryption is exactly the one used by smproxy://crypt1/… links — AES-256-GCM, nonce || ciphertext || tag, base64-encoded, under the same shared key. A panel that already issues those links needs no new code: encrypt the value, prefix it with crypt1/, and put it in the header.

To get an encrypted value without writing any code, use the same generator that produces the links — it returns smproxy://crypt1/<encrypted>; take everything after smproxy://, that is crypt1/<base64>, exactly what the header expects.

POST https://provider.smproxy.io/public/crypto-link { "url": "https://example.com/routing.json" } → { "link": "smproxy://crypt1/AbCd…" } routing-update-url: crypt1/AbCd… s-dns: crypt1/AbCd…
Encrypted link generator →

Both separators are accepted — crypt1/<base64> and crypt1:<base64> — but use the slash: it matches the deep links, so there is one format to remember.

This is obfuscation, not secrecy. The key ships inside every client, so anyone who takes the app apart can read these values. It keeps the profile URL out of plain sight — away from a casual glance at traffic, or from being pasted into a chat — and nothing more. Do not put anything genuinely sensitive behind it.

Deep links

Two schemes are accepted: smproxy:// (current) and smartvpn:// (pre-rename, kept working for links already handed out). That applies to clickable links only — the OS hands a link to the app registered for its scheme. Inside the routing / autorouting headers any scheme word is accepted (section 07a).

Link
Effect
smproxy://add?url=<encoded URL>
Add a subscription
smproxy://add/<https://…> · add/<base64 URL>
Same, path form
smproxy://crypt1/<base64>
Add from an encrypted payload
smproxy://routing/add/<base64|url>
Import a routing profile, don't switch to it
smproxy://routing/onadd/<base64|url>
Import and activate
smproxy://autorouting/add|onadd/<url>
Same, plus keep it updated from that URL
smproxy://routing/off
Turn routing off

The payload may also be passed as ?data=<base64>. For routing links the target may hold the profile JSON, its base64, or another smproxy:// link — all three resolve.

↑ Back to contents
11

Provider portal

Self-service at provider.smproxy.io — registration, statistics, messages and link generators.

URL
Purpose
provider.smproxy.io/provider/register
Registration (email + password or Google)
provider.smproxy.io/provider/login
Sign in
provider.smproxy.io/provider/dashboard
Your Provider ID + set a custom ID
provider.smproxy.io/provider/stats
Usage statistics
provider.smproxy.io/provider/messages
Compose in-app messages (section 06)
provider.smproxy.io/provider/hwid-link
Generate a device-bound smproxy://addhw/… link
provider.smproxy.io/provider/crypto-link
Generate an encrypted smproxy://crypt1/… link
Open a provider account

Registration takes a minute and gives you a Provider ID, statistics and message broadcasting. Free.

Statistics in 3 steps
1Register
2Copy your Provider ID (or set a custom one)
3Add it to the subscription as the response header providerid / s-providerid

Your users' apps will start populating the statistics you see in the portal.

↑ Back to contents
12

Pricing — everything is free right now

SMProxy App is in active development and open testing, so all features are free right now and will stay that way for a long time: subscription import, all section-04 headers, device-bound (addhw) and encrypted (crypt1) links, auto-update, kill-switch, split-tunnel, themes, statistics and messages/push. There are no paid plans and no locked features.

Subscription import All response headers addhw & crypt1 Auto-update Kill-switch Split-tunnel Themes Statistics Messages / push
🌱 In the future, once the project leaves the testing stage, some features may change. We'll announce any changes openly and in advance — what's available now will stay free for the whole beta period.

🔒Fields gated by provider_active

Locked keys are provider-controlled branding/behaviour (site/Telegram buttons, renewal UI, fallback-url, migration and hide-settings). They apply only when the response contains providerid and it has provider_active == true.

If the provider is not active (no providerid, unknown, or provider_active == false), these fields are force-reset on every update (so a foreign/stale provider can't swap buttons or fallback). They're restored as soon as the provider is active again.

Not gated (always work): profile-title, s-title, profile-update-interval, subscription-userinfo, announce, support-url, profile-web-page-url, providerid, geoipurl/geositeurl.

↑ Back to contents
13

What needs an active Provider ID

One place to check instead of hunting for the 🔒 marks. Active means the Provider ID sent with the subscription belongs to a provider account that is currently active. Without it the keys in the first table are simply ignored — the subscription itself keeps working.

🔒Requires an active Provider ID

Area
Keys
Bypass
s-fragment s-noise s-noises s-resolve s-dns — and their INCY/Happ equivalents fragmentation-* noises-* server-address-resolve-*
Migration & fallbacks
new-url new-domain fallback-url fallback-domains
Provider branding
s-siteurl s-sitename s-tgbot x-tgbot sort-order serverDescription
Banner & contacts
banner-text banner-button-text banner-button-url banner-bg-color banner-button-color announce-url support-email
Expiry UI
sub-expire s-showexpire sub-expire-button-text sub-expire-button-link-site sub-expire-button-link-tg notification-subs-expire
Access
hide-settings

Works with or without a Provider ID

Key
Note
profile-title / s-title
Subscription name
profile-update-interval
Auto-refresh interval
profile-web-page-url
Provider web page
announce
The announcement text itself — but announce-url (making it clickable) needs an active provider
support-url
Support link
subscription-userinfo
Quota and expiry date
subid
Stable subscription id
routing autorouting routing-update-url routing-update-interval routing-profile
Routing profiles, including the deep-link command forms — a profile carries rules only, so it needs no active provider
geoipurl / geositeurl
Geo database addresses — subscription response only

The same gating applies however the value arrives — an HTTP header, a #key: line in the body, or the provider account.

↑ Back to contents
This reference reflects current app behaviour. Verify exact format/base64 rules for individual headers against the server before publishing.