Skip to content

Subscription JSON Template

Alireza Ahmadi edited this page Sep 12, 2026 · 1 revision

Subscription JSON Template

Settings → Subscription → JSON

This template controls what …/sub/<client name>?format=json returns — the sing-box configuration your users' clients download.


The one thing to know first

The template is not a sing-box configuration. It is a settings object that the panel merges with the outbounds it generates for each client. The result is a sing-box configuration; the template is the part you supply.

That distinction has a practical consequence: the template is flatter than a sing-box config. Routing options are written at the top level, and the panel assembles the route section itself.

// ✅ Correct — rule_set at the top level
{
  "rules": [  ],
  "rule_set": [  ],
  "final": "proxy"
}

// ❌ Wrong — the panel does not read a nested route object
{
  "route": {
    "rules": [  ],
    "rule_set": [  ]
  }
}

Pasting a complete sing-box config into the template is the most common mistake. Keys the panel does not recognize are ignored silently, so the result is a config that is missing exactly the parts you cared about — typically rule-sets, which then produce:

initialize dns router: dns rule[2]: rule-set not found: direct-geosite

The panel's form is the intended way to edit the template and always produces the right shape. The raw JSON editor is there for options the form does not cover, and expects that same shape.


Supported keys

Anything not in these two tables is ignored.

Copied to the root of the generated config

Key Notes
log sing-box log options
dns The complete DNS section: servers, rules, final, strategy
inbounds Replaces the default TUN + mixed inbounds entirely
experimental Clash API, cache file, and so on
http_clients Named HTTP clients for downloading remote rule-sets

Assembled into route by the panel

Key Notes
rules Replaces the default rules entirely, in the order you write them
rule_set Rule-set definitions, local or remote
final Outbound for unmatched traffic. Defaults to proxy
default_domain_resolver DNS server that resolves outbound server domains
default_http_client HTTP client used by remote rule-sets
override_android_vpn Passed through as-is

What the panel generates for you

You do not write these, and writing them has no effect:

outbounds — always built from the client's inbounds and any external links, plus three entries the panel adds at the front:

Tag Type Contents
proxy selector auto, direct, then every node
auto urltest Every node, tested against http://www.gstatic.com/generate_204 every 10m
direct direct

Refer to proxy, auto and direct from your rules; they are always present. Node tags come from the inbound tags, with -2, -3 suffixes only if two would otherwise collide.

route.auto_detect_interface — always enabled.

route.rules — when the template defines no rules, the panel supplies a default set: sniffing, then Clash Direct mode → direct, then Clash Global mode → proxy. As soon as you define rules yourself, none of those defaults are added. Include what you want explicitly — {"action": "sniff"} in particular is easy to forget and changes how domain-based rules behave.

route.default_domain_resolver — when you leave it unset and the template has more than one DNS server, the panel fills it in with dns.final (or the first server) so sing-box does not have to guess and log a deprecation warning. Set it explicitly to control which server resolves outbound server domains.

http_clients — when a remote rule-set does not name an HTTP client and the template declares none, the panel adds a client tagged default and points route.default_http_client at it. This only avoids a deprecation warning from sing-box 1.14; if you declare your own http_clients, the panel leaves the decision entirely to you.


Examples

Minimal

Everything omitted, so every default applies: default inbounds, default rules, no DNS section, final: proxy.

{}

Routing by rule-set

The scenario from the top of this page, written correctly. Note that rules and rule_set sit next to log, not inside a route object.

{
  "log": { "level": "info", "timestamp": true },
  "rules": [
    { "action": "sniff" },
    { "protocol": "dns", "action": "hijack-dns" },
    { "ip_is_private": true, "action": "route", "outbound": "direct" },
    { "rule_set": "geosite-ir", "action": "route", "outbound": "direct" },
    { "rule_set": "geoip-ir", "action": "route", "outbound": "direct" },
    { "rule_set": "category-ads", "action": "reject" }
  ],
  "rule_set": [
    {
      "type": "remote",
      "tag": "geosite-ir",
      "format": "binary",
      "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-ir.srs",
      "download_detour": "direct"
    },
    {
      "type": "remote",
      "tag": "geoip-ir",
      "format": "binary",
      "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-ir.srs",
      "download_detour": "direct"
    },
    {
      "type": "remote",
      "tag": "category-ads",
      "format": "binary",
      "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-category-ads-all.srs",
      "download_detour": "direct"
    }
  ],
  "final": "proxy"
}

DNS that follows the routing

A split-DNS setup: domains routed to direct resolve locally, everything else resolves through the proxy. The rule_set tags referenced from dns.rules are the ones defined at the top level — the same definitions the routing rules use.

{
  "dns": {
    "servers": [
      { "tag": "proxy-dns", "type": "tcp", "server": "8.8.8.8", "server_port": 53, "detour": "proxy", "domain_resolver": "local-dns" },
      { "tag": "direct-dns", "type": "local" },
      { "tag": "local-dns", "type": "local" }
    ],
    "rules": [
      { "clash_mode": "Direct", "action": "route", "server": "direct-dns" },
      { "clash_mode": "Global", "action": "route", "server": "proxy-dns" },
      { "rule_set": "geosite-ir", "action": "route", "server": "direct-dns" }
    ],
    "final": "proxy-dns",
    "strategy": "prefer_ipv4"
  },
  "rules": [
    { "action": "sniff" },
    { "protocol": "dns", "action": "hijack-dns" },
    { "rule_set": "geosite-ir", "action": "route", "outbound": "direct" }
  ],
  "rule_set": [
    {
      "type": "remote",
      "tag": "geosite-ir",
      "format": "binary",
      "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-ir.srs",
      "download_detour": "direct"
    }
  ],
  "default_domain_resolver": "local-dns",
  "final": "proxy"
}

default_domain_resolver is set explicitly here because proxy-dns resolves through the proxy, and resolving the proxy's own server address through the proxy cannot work.

Custom inbounds

Replacing the defaults — a TUN interface tuned for a router, and a mixed proxy on the LAN. Note that inbounds replaces the default list wholesale; there is no merging.

{
  "inbounds": [
    {
      "type": "tun",
      "address": ["172.19.0.1/30"],
      "mtu": 1500,
      "auto_route": true,
      "strict_route": true,
      "stack": "mixed"
    },
    {
      "type": "mixed",
      "listen": "0.0.0.0",
      "listen_port": 2080
    }
  ],
  "final": "proxy"
}

Clash API for a dashboard

{
  "experimental": {
    "clash_api": {
      "external_controller": "127.0.0.1:9090",
      "external_ui": "ui",
      "secret": "",
      "default_mode": "rule"
    },
    "cache_file": { "enabled": true, "store_fakeip": false }
  }
}

Verifying the result

The panel does not validate the template against the sing-box schema. sing-box check does that correctly and always matches the version you are running:

curl -o config.json "http://<your server>:2096/sub/<client name>?format=json"
sing-box check -c config.json

It reports the exact field and rule index that is wrong. A message like:

initialize dns router: dns rule[2]: rule-set not found: direct-geosite

means something references a rule-set that is not in the generated config — almost always because the definition was written somewhere the panel does not read it. Check that rule_set is at the top level of your template, then run check again.

To see what the panel actually produced, compare the downloaded config.json with your template: keys that vanished were not recognized.