For VPN providers
Moving users from Happ to Rameta
Rameta understands Happ subscriptions as-is: same request and response headers, same body formats and links. The Remnawave HWID limit works unchanged. Below is everything you need so users can simply tap “Open in Rameta”.
1. Quick start
0 minutes
Do nothing: Remnawave subscriptions already work in Rameta (the “Fallback Base64” rule returns links).
2 minutes
Add the Rameta response rule → users get full Xray JSON with all transport settings. →
Button
Add a button to your subscription page with rameta://add/{{SUBSCRIPTION_LINK}}.
Branding
Register and send the header providerid — logo, color and routing profile in the app. →
2. What Rameta sends
Every subscription update is a plain GET with these headers:
| Header | Example | Purpose |
|---|---|---|
User-Agent | Rameta/1.0.0/ios/42 | Rameta/<version>/ios/<build>. In Remnawave rules use the regex ^Rameta/. |
X-HWID | 5F0C2A4E-… | Stable device id (kept in Keychain). Remnawave counts devices by it — the HWID device limit works just like with Happ. |
X-Device-OS | iOS | Platform (shown in the Remnawave device list). |
X-Ver-OS | 18.1 | iOS version. |
X-Device-Model | iPhone16,2 | Device model. |
X-Device-Locale | ru | UI language — lets you localize titles and announcements. |
X-App-Version | 1.0.0 | Rameta version. |
X-Age-Public-Key | age1pq1… | Per-device age public key (hybrid post-quantum ML-KEM-768 + X25519). Lets you encrypt the body for this device only. |
x-hwid, x-device-os, x-ver-os and x-device-model — the HWID Device Limit counts Rameta devices exactly like Happ. When the limit is reached the panel answers with x-hwid-max-devices-reached: true and an announce, and Rameta shows it to the user.3. Response headers
Same set and values as Happ. In Remnawave they live in “Subscription settings → custom response headers” and reach Happ and Rameta alike.
| Header | Meaning |
|---|---|
profile-title | Subscription name. Plain text or base64:… (UTF-8). |
subscription-userinfo | upload=…; download=…; total=…; expire=<unix> — traffic and expiry. |
profile-update-interval | Auto-update interval, hours. |
support-url | Support link (https://, tg://, mailto:). |
profile-web-page-url | Subscription / account web page. |
announce | Announcement up to 200 chars; base64:… allowed. |
routing | Routing profile link: happ://routing/… or rameta://routing/… (add — offer, onadd — enable immediately, off — disable). |
providerid | Provider id from the Rameta registry (register) — the app loads your logo, color, announcement and routing profile. |
hide-settings | 1 — hide server settings of this subscription from the user. |
sub-info-color · sub-info-text · sub-info-button-text · sub-info-button-link | Subscription info banner: color (red/blue/green), text, button and its link. |
sub-expire · sub-expire-button-link | Warn about expiry and where the “Renew” button leads. |
new-url | Permanently replace the subscription URL. |
new-domain | Replace only the domain of the subscription URL. |
fallback-url | Backup URL used when the main one fails. |
subscription-autoconnect · subscription-autoconnect-type | Auto-connect after update and how the server is chosen. |
subscription-auto-update-enable | Enable/disable auto-update. |
ping-type · check-url-via-proxy | Latency test method and the URL tested through the proxy. |
fragmentation-* · noises-* · mux-* | TLS ClientHello fragmentation, noises and mux for share links — same keys and values as Happ. |
x-hwid-max-devices-reached · x-hwid-not-supported · x-hwid-limit | Sent by Remnawave when the device limit is hit — Rameta shows a clear message and the announce. |
The same keys may also be sent in the body as leading #key: value lines, and in the URL fragment:
https://sub.example.com/abcdef#Мой VPN?profile-update-interval=6&support-url=https://t.me/myvpn_support
If a key is set in several places, the body line wins, then the response header, then the URL fragment (same as Happ).
4. Subscription formats
- Share links — base64 (standard or URL-safe, padded or not) or plain text, one per line:
vless://,vmess://,trojan://,ss://,socks://,hysteria2:///hy2://. - Xray JSON — an array of full configs (each with
remarks) or a single config. Outbounds, routing, dns and balancers are used; Rameta supplies its own inbounds. - age-encrypted body — ASCII-armored (
-----BEGIN AGE ENCRYPTED FILE-----) or binary; decrypts to any format above. → - Remnawave server descriptions (serverDescription):
meta.serverDescriptionin JSON, and a?serverDescription=<base64>suffix after the name in links.
vless://uuid@de1.example.com:443?type=tcp&security=reality&sni=www.microsoft.com&pbk=…&sid=…&fp=chrome&flow=xtls-rprx-vision#🇩🇪 Germany
trojan://password@nl.example.com:443?security=tls&sni=nl.example.com#🇳🇱 Netherlands
ss://YWVzLTEyOC1nY206cGFzcw@fi.example.com:8388#🇫🇮 Finland
hy2://auth@jp.example.com:443?sni=jp.example.com#🇯🇵 Japan
Body with metadata lines:
#profile-title: base64:0JzQvtC5IFZQTg==
#profile-update-interval: 6
#support-url: https://t.me/myvpn_support
#announce: Новые серверы в Казахстане!
vless://…#🇩🇪 Germany
vless://…#🇳🇱 Netherlands
5. Remnawave
Response Rule for Xray JSON
Panel → Subscription → Response Rules. Add this rule after “Browser Subscription” and before “Fallback Base64” (rules are checked top-down, the first match wins):
{
"name": "Rameta",
"enabled": true,
"operator": "AND",
"conditions": [
{
"headerName": "user-agent",
"operator": "REGEX",
"value": "^Rameta/",
"caseSensitive": true
}
],
"responseType": "XRAY_JSON",
"responseModifications": {
"additionalExtendedClientsRegex": [
"^Rameta/"
]
}
}
XRAY_JSON— Rameta receives the full Xray config of every host: XHTTP, Reality, fragmentation, mux and host templates are kept.additionalExtendedClientsRegex— Remnawave treats Rameta as an “extended client” like Happ and INCY and passes server descriptions (serverDescription).
Subscription page button
In the Remnawave Subscription Page settings add Rameta for iOS with an “add subscription” button:
rameta://add/{{SUBSCRIPTION_LINK}}
The App Store link will be published at release; until then use TestFlight.
6. Encrypted subscriptions
Remnawave can encrypt the response body with age. Add encryption with the Rameta public key (method age1, X25519) to the Rameta rule:
{
"additionalExtendedClientsRegex": [
"^Rameta/"
],
"encryption": {
"method": "age1",
"key": "age1zaymjnnxuh8xsmrw9cjajm4gqnjsgtxf6e8v54zx3rpejqsluyls0eymfz"
}
}
Rameta public key: age1zaymjnnxuh8xsmrw9cjajm4gqnjsgtxf6e8v54zx3rpejqsluyls0eymfz (always current at /v1/app/config → agePublicKey).
A custom backend can also encrypt per device with the key from X-Age-Public-Key (age1pq1…). Remnawave uses a static key per rule, so use age1 with the Rameta key there.
7. Deep links
| Link | Action |
|---|---|
rameta://add/<url> | Add a subscription. The URL may be raw or URL-encoded. |
rameta://crypt/<payload> | Add a subscription from an encrypted link (below). |
rameta://routing/add/<base64> | Offer a routing profile (base64 JSON). |
rameta://routing/onadd/<base64> | Add a profile and enable it immediately. |
rameta://routing/off | Disable the routing profile. |
rameta://connect · rameta://disconnect · rameta://toggle | Connect, disconnect, toggle the VPN (handy for iOS Shortcuts). |
happ://add/… · happ://routing/… | Happ links pasted from the clipboard or a QR code are accepted too. |
8. Routing profiles
The format is Happ-compatible: the JSON below, base64-encoded, is delivered as rameta://routing/add/… (or onadd), via the routing header or your provider card.
| Key | Meaning |
|---|---|
Name | Profile name. |
GlobalProxy | "true" — everything not matched goes via VPN; "false" — direct. |
RemoteDNSType · RemoteDNSDomain · RemoteDNSIP | DNS via VPN: DoH or DoU (UDP), DoH URL and server IP. |
DomesticDNSType · DomesticDNSDomain · DomesticDNSIP | DNS for direct connections. |
Geoipurl · Geositeurl | Where to download geoip.dat / geosite.dat. Use Rameta's trimmed files (below). |
LastUpdated | Unix time of the profile version; a newer value makes devices update it. |
DnsHosts | Static records {"domain": "IP"}. |
DirectSites · DirectIp | Direct. |
ProxySites · ProxyIp | Via VPN. |
BlockSites · BlockIp | Block. |
DomainStrategy | AsIs · IPIfNonMatch · IPOnDemand |
FakeDNS | "true"/"false" — FakeDNS for domain rules. |
{
"Name": "Мой VPN — РФ напрямую",
"GlobalProxy": "true",
"RemoteDNSType": "DoH",
"RemoteDNSDomain": "https://cloudflare-dns.com/dns-query",
"RemoteDNSIP": "1.1.1.1",
"DomesticDNSType": "DoU",
"DomesticDNSDomain": "",
"DomesticDNSIP": "77.88.8.8",
"Geoipurl": "https://vpn.rameta.net/geo/geoip.dat?tags=ru,private",
"Geositeurl": "https://vpn.rameta.net/geo/geosite.dat?tags=category-ru,private",
"LastUpdated": "1761609600",
"DnsHosts": {},
"DirectSites": [
"geosite:category-ru",
"geosite:private",
"domain:ru",
"domain:xn--p1ai"
],
"DirectIp": [
"geoip:ru",
"geoip:private"
],
"ProxySites": [
"domain:youtube.com",
"keyword:openai"
],
"ProxyIp": [
"91.108.4.0/22"
],
"BlockSites": [
"geosite:category-ads"
],
"BlockIp": [],
"DomainStrategy": "IPIfNonMatch",
"FakeDNS": "false"
}
Domain rules use Xray syntax: domain: (domain and subdomains), full: (exact), keyword:, regexp:, geosite:. IPs: address, CIDR or geoip:. Order: block → proxy → direct → default (GlobalProxy). Rameta also accepts RouteOrder (default block-proxy-direct).
# make a link from a profile file
echo "rameta://routing/onadd/$(base64 -w0 profile.json)"
Ready-made profiles
The same profiles are served by /v1/presets together with 50+ services and domain zones.
| Profile | Description | Geo data | |
|---|---|---|---|
Russian sites directru-direct | Everything via VPN except Russian sites and IPs. | geosite:category-ru, geosite:private, geosite:ru-available-only-inside, geoip:private, geoip:ru | Open |
Russian sites direct + ad blockingru-direct-noads | Russian sites direct plus blocking of major ad networks (light list, ~800 domains). | geosite:category-ads, geosite:category-ru, geosite:private, geosite:ru-available-only-inside, geoip:private, geoip:ru | Open |
Everything via VPNall-proxy | All traffic via VPN; only the local network goes direct. | — | Open |
Only blocked sites via VPNblocked-only | Only sites blocked in Russia (and geo-blocked AI services) go via VPN; everything else direct. | geosite:category-ai-!cn, geosite:ru-blocked, geoip:ru-blocked-community | Open |
Everything via VPN + ad blockingads-block | Everything via VPN with full ad/tracker blocking (geosite:category-ads-all, ~150k domains; adds ~20 MB to the VPN extension memory, iOS limit is 50 MB). | geosite:category-ads-all | Open |
Only AI services via VPNai-only | Only foreign AI services via VPN; everything else direct. | geosite:category-ai-!cn | Open |
9. Trimmed geo files
The iOS VPN extension has ~50 MB of memory while the full geosite.dat is ~70 MB. So Rameta (and your profile) fetches only the entries it needs:
https://vpn.rameta.net/geo/geosite.dat?tags=category-ru,private,category-ads
https://vpn.rameta.net/geo/geoip.dat?tags=ru,private
Source: runetfreedom/russia-v2ray-rules-dat (updated daily). Useful tags: category-ru, ru-blocked, ru-available-only-inside, category-ads-all, category-gov-ru, category-ai-!cn; geoip: ru, private, ru-blocked, ru-blocked-community, telegram. Full list: /geo/tags.
10. Provider registry
Register so Rameta shows your logo, color, announcement and offers your routing profile. Then send providerid: <your id> in the subscription. The “verified” badge is granted manually after we contact you.
11. Crypt link generator
Turns a subscription URL into rameta://crypt/…: the payload is the binary age ciphertext for the Rameta public key, base64url without =. The URL is neither stored nor logged.
Same via the API, or locally without our server:
curl -s https://vpn.rameta.net/v1/crypt -H 'Content-Type: application/json' \
-d '{"url":"https://sub.example.com/abcdef"}'
# → {"link":"rameta://crypt/YWdlLWVuY3J5cHRpb24ub3JnL3Yx…","armored":"-----BEGIN AGE ENCRYPTED FILE-----…"}
# Rameta public key: https://vpn.rameta.net/v1/app/config → agePublicKey
URL='https://sub.example.com/abcdef'
printf '%s' "$URL" | age -r age1zaymjnnxuh8xsmrw9cjajm4gqnjsgtxf6e8v54zx3rpejqsluyls0eymfz | basenc --base64url | tr -d '=\n' | sed 's|^|rameta://crypt/|'
12. API
Base: https://vpn.rameta.net. Responses are JSON; errors: {"error":"…","message":"…"}.
| Method | Description |
|---|---|
GET /v1/app/config | Versions, ping URL, geo base, presets version, age public key. |
GET /v1/presets | Services, domain zones and ready profiles (ETag). |
GET /geo/tags | All geosite and geoip tags. |
GET /geo/geosite.dat?tags=… | Trimmed geosite.dat (ETag, Last-Modified). |
GET /geo/geoip.dat?tags=… | Trimmed geoip.dat. |
POST /v1/crypt | {"url"} → {"link","armored"}; 30 req/min per IP. |
POST /v1/providers | Register → {"id","adminToken"}. |
GET /v1/providers/{id} | Public provider card. |
PUT /v1/providers/{id} | Update (Authorization: Bearer <adminToken>). |
GET /healthz | {"ok":true} |
Updating your provider card:
curl -X PUT https://vpn.rameta.net/v1/providers/AbC123xY \
-H 'Authorization: Bearer <adminToken>' -H 'Content-Type: application/json' \
-d '{"logoUrl":"https://example.com/logo.png","color":"#7C3AED","announce":"Скидка 20% до пятницы",
"routingProfile":{"id":"my-ru","name":"РФ напрямую","globalProxy":true,
"direct":["geosite:category-ru","domain:ru"],"directIp":["geoip:ru","geoip:private"],
"proxy":[],"proxyIp":[],"block":[],"blockIp":[],"domainStrategy":"IPIfNonMatch"}}'
PUT fields: name, website, supportUrl, logoUrl (https), color (#RRGGBB), announce (≤200), routingProfile (object as in /v1/presets, or null), contact, subscriptionDomains. Missing fields are untouched, null clears.