At a glance

First identify what you have: a subscription URL, an encoded list of links, or a complete core config. This guide is for anyone facing an empty subscription import, an unrecognized JSON file, or settings lost during conversion. It covers VMess and VLESS fields, local Base64 conversion, client imports, and mapping to Xray config—plus the settings that share links cannot preserve.

1. How subscription URLs, Base64, and JSON differ

“V2Ray subscription” does not refer to one specific file format. The HTTPS URL you receive is just the access point; the client fetches the actual response from it. That response may be a list of share links, one per line, or text containing the entire list encoded in Base64. Some services return different content depending on the request headers, so the URL suffix alone does not tell you the format.

ItemHow to identify itHow to handle it
Subscription URLUsually starts with https:// and may contain an access tokenAdd it to a subscription group and update as needed
Plain-text link listEach line starts with a protocol prefix such as vmess:// or vless://Import multiple links from the clipboard
Base64-encoded subscription bodyUsually a long string of encoded characters that decodes to share linksLet the subscription parser handle it, or decode it locally first
JSON inside a VMess linkCommon fields include v, add, id, and netUsed to create or parse a single VMess share link
Native Xray configCommon top-level fields include inbounds, outbounds, and routingLoad it with the matching core or through a custom config option

Just because two items are JSON does not mean they are interchangeable. A VMess share object describes one server and its transport settings; an Xray config tells the core how to listen on local ports, create outbound connections, and apply routing. Saving the former as config.json does not turn it into the latter. Base64-encoding the entire config does not make it a subscription a client can use either.

2. VMess and VLESS share link fields

A common VMess share link consists of vmess:// followed by Base64-encoded JSON. The example below shows only the decoded JSON. All domains and UUIDs are fictional and for illustration only; they cannot be used to connect. v: "2" identifies the share object format, not the client or core version.

{
  "v": "2",
  "ps": "Example VMess WS",
  "add": "edge.example.com",
  "port": "443",
  "id": "00000000-0000-4000-8000-000000000001",
  "aid": "0",
  "scy": "auto",
  "net": "ws",
  "type": "none",
  "host": "edge.example.com",
  "path": "/ws",
  "tls": "tls",
  "sni": "edge.example.com"
}

add is the server address, port is the remote port, and id is the user ID. net, host, and path specify the transport and WebSocket request settings. aid: "0" corresponds to the common VMess AEAD configuration in use today; do not change it to an arbitrary value just to silence an error. The example’s type: "none" is not a switch for disabling TLS.

VLESS share links usually use a URI format. The identity goes before @, the server and port follow it, and transport settings go in the query string. This example is also nonfunctional; keep the link on a single, complete line:

vless://[email protected]:443?encryption=none&security=tls&type=tcp&sni=edge.example.com#demo-vless

VMess example fields

Address and port
add and port
Identity
id
Transport
net: "ws"
Request path
path: "/ws"
TLS name
sni

These fields are common in share objects, but their names and hierarchy change when mapped to a core config.

VLESS example parameters

User ID
UUID before @ in the URI
Protocol encryption
encryption=none
Connection security
security=tls
Transport
type=tcp
TLS name
sni=edge.example.com

encryption=none does not disable TLS; they apply at different layers.

URI parameter values must be encoded according to URL rules. For example, the path /ws?ed=2048 used as a parameter value should be written as path=%2Fws%3Fed%3D2048, so its question mark is not mistaken for the start of another query string. Encode notes containing non-ASCII characters, spaces, or special symbols correctly too. Do not percent-encode the entire URI at once, or the protocol prefix and separators will be corrupted.

3. Decode and re-encode Base64 subscriptions locally

A typical conversion is “multiple share links → join with line breaks → Base64-encode the entire text.” If the list contains VMess links, there are two encoding layers: the outer layer wraps the list, and the inner layer wraps each VMess JSON object. Once decoding the outer layer reveals vmess://, split the text by line instead of treating the entire result, protocol prefixes included, as Base64 again.

3 → 4
Base64 represents every 3 bytes with 4 characters, excluding line-break separators
443
The remote TLS port in this example; not all nodes use it
10808
The local SOCKS port used in the config below; check that it is available

Run the following plain JavaScript in the developer console of a local blank page. Read the code first, and put your own data only in local variables; it makes no network requests. The encoder uses UTF-8 to avoid errors when Chinese notes are passed directly to btoa(). The decoder handles both standard Base64 and common URL-safe variants.

function encodeSubscription(text) {
  const bytes = new TextEncoder().encode(text);
  let binary = "";
  for (const byte of bytes) {
    binary += String.fromCharCode(byte);
  }
  return btoa(binary);
}

function decodeSubscription(text) {
  let encoded = text.trim()
    .replace(/\s+/g, "")
    .replace(/-/g, "+")
    .replace(/_/g, "/");

  if (!/^[A-Za-z0-9+/]*={0,2}$/.test(encoded)) {
    throw new Error("The content is not a valid Base64 character sequence");
  }

  encoded = encoded.replace(/=+$/, "");
  if (encoded.length % 4 === 1) {
    throw new Error("Invalid Base64 length; check that the content was copied in full");
  }

  encoded += "=".repeat((4 - encoded.length % 4) % 4);
  const bytes = Uint8Array.from(atob(encoded), char => char.charCodeAt(0));
  return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
}

const demoLink =
  "vless://[email protected]:443"
  + "?encryption=none&security=tls&type=tcp&sni=edge.example.com#demo-vless";

const encodedDemo = encodeSubscription(demoLink);
console.log(encodedDemo);
console.log(decodeSubscription(encodedDemo));

4. Import options in v2rayN and v2rayNG

Choose the import option based on what you have. Add a subscription URL to a subscription group, import one or more share links from the clipboard, and load a complete core JSON through a custom config option. Menu locations may vary by desktop and Android version. The labels below reflect common menu names; if they differ slightly, use the equivalent import function. Do not enter a subscription URL in the server address field.

v2rayN: Create a group, then update the subscription

  1. Create a group

    In the main window, open “Subscription Group” → “Subscription Group Settings,” click “Add” to create a group, enter a label and subscription URL, then save and return to the server list.

  2. Fetch the content

    From the “Subscription Group” menu, choose “Update Current Subscription (without proxy)” or the equivalent option to update through a proxy. Use the first option when your current network can reach the subscription service directly. The second requires a working proxy and a correctly configured proxy route.

  3. Import links

    If you only have share link text, copy the complete links and choose “Servers” → “Import batch URLs from clipboard.” On common desktop versions, you can also use Ctrl+V. This adds a snapshot of the nodes; it does not create a link for future subscription updates.

  4. Check the settings

    Open each imported entry for editing and check the server address, port, transport protocol, TLS, and SNI. Seeing an entry in the list is not enough: start it and check the core log at the bottom for config parsing errors.

v2rayNG: Handle subscriptions and clipboard imports separately

If you decode a subscription and import the links manually, later changes to server addresses, ports, or credentials on the server will not automatically update your local snapshot. To keep receiving updates, retain the original subscription group. When migrating, back up the subscription URL and local custom settings separately instead of saving only a list of share links.

5. Map share links to native Xray JSON

When a client generates a core config, it does more than rename fields: it also adds runtime details such as local inbounds, outbound tags, logs, and routing. Below, the VLESS TCP + TLS example from earlier is mapped to a minimal SOCKS config. It listens only on 127.0.0.1:10808; the remote host is still a fictional teaching domain, so this config is for understanding the structure only and will not connect.

{
  "log": {
    "loglevel": "warning"
  },
  "inbounds": [
    {
      "tag": "socks-in",
      "listen": "127.0.0.1",
      "port": 10808,
      "protocol": "socks",
      "settings": {
        "auth": "noauth",
        "udp": true
      }
    }
  ],
  "outbounds": [
    {
      "tag": "proxy",
      "protocol": "vless",
      "settings": {
        "vnext": [
          {
            "address": "edge.example.com",
            "port": 443,
            "users": [
              {
                "id": "00000000-0000-4000-8000-000000000001",
                "encryption": "none"
              }
            ]
          }
        ]
      },
      "streamSettings": {
        "network": "tcp",
        "security": "tls",
        "tlsSettings": {
          "serverName": "edge.example.com"
        }
      }
    }
  ]
}
Share link valueCorresponding JSON fieldNotes
Server and :443settings.vnext[0].address and portJSON ports are numbers
UUIDsettings.vnext[0].users[0].idKeep the full value; a note cannot replace it
encryption=noneusers[0].encryptionBelongs in VLESS user settings, not TLS settings
type=tcpstreamSettings.networkThe current core must support the transport name and syntax
security=tls and snistreamSettings.security and tlsSettings.serverNameDo not guess the SNI from the server IP
#demo-vlessUsually stored as a client labelThe label may not be included in the runtime config

The local port 10808 is the specific example value set here; do not assume every client listens on this port by default. Before running the config, make sure no other program is using it. Binding only to the loopback address prevents an unauthenticated SOCKS inbound from being exposed to your LAN. Do not change it to listen on all interfaces to troubleshoot a remote connection.

When exporting a full JSON config back to a share link, clients can usually extract only one compatible proxy outbound. The relationships among routing.rules, DNS, multiple inbounds, and multiple outbounds cannot be fully represented by a single-node share link. If the original config depends on these settings, keep the JSON; exporting a QR code does not complete a full migration.

6. Troubleshoot empty imports, decode errors, and missing fields

Troubleshoot in stages: “fetch → decode → parse → run,” and record the result at each step. A successful subscription update only means the client processed something; generating entries, preserving all fields, starting the core, and reaching the remote server are separate checks. Fix the earliest failing step first. Changing TLS, ports, and the subscription URL all at once makes it harder to compare results.

The subscription URL opens. Why is the imported list empty?

Check the actual response content, not just whether the request succeeded. If it is a web page, an access warning, or a format unsupported by your client, ask the subscription provider which format to use. If it decodes to share links, copy one complete link and test it through the clipboard import option to distinguish a response format issue from a problem with an individual link.

Base64 decoding reports invalid characters or length. What should I do?

First, make sure you are decoding the subscription URL itself neither by mistake nor with quotation marks, code fences, or ellipses included. Remove whitespace used as separators from the body, and check for URL-safe characters and trailing padding. If the content was truncated, adding a few equal signs cannot restore the missing data; retrieve the complete body again.

The JSON passes syntax validation. Why does the core still report an error?

Valid syntax only means the commas, quotes, and brackets are well-formed; it does not mean the core supports the field structure. First confirm that it is an Xray runtime config, not a VMess share object. Then check the core log for the field, transport type, or missing file it identifies, and make sure the imported config matches the core in use.

Why won't the node connect after exporting and reimporting it, even though its name is unchanged?

A name is not a connection parameter. Compare the address, port, UUID, transport, path, Host, and SNI one by one, along with Flow and Reality settings if present. Do not disable certificate verification as a substitute for checking these values. If the exporter dropped a required field, restore from a full config backup or the original subscription.

Why did my local node changes revert after updating the subscription?

Subscription updates usually refresh their entries to match the remote content, and local changes may not be retained. Copy entries you need to maintain independently into a non-subscription group and keep a clear record of your changes. For server-managed nodes, make changes at the subscription source where possible to avoid repeating manual edits after every update.

7. Verify and back up after conversion

The safest migration does not rely on “one really long string,” but on three clearly defined items: the original subscription URL for future updates, share links for exchanging nodes, and a full config or client backup for restoring local behavior. Keep all three if needed, but do not treat them as interchangeable. Store backups containing credentials in a local location with access controls.

  1. Keep the original: Save the original response or file before converting, so you are not left with only a result that has been processed multiple times.
  2. Check one entry: Compare the protocol, address, port, and transport settings for a single node first. Make sure UUID, Host, and SNI have not been confused with one another.
  3. Check startup: Enable the entry and review the core log. Resolve config parsing and port conflicts first, then troubleshoot certificates, handshakes, and remote connectivity.
  4. Check the app: Desktop apps also need to use the system proxy or the specified local proxy port correctly. On Android, make sure the connection is running and grant the VPN permission requested by the system.
  5. Test updates: If migration includes a subscription, manually update it once in the target client and confirm new entries appear in the intended group rather than only as a batch of static nodes.