Guides, Nov 10, 2026

# Hysteria2 link format: every hy2:// parameter

A Hysteria2 link is hysteria2://auth@host:port/?key=value#name, and hy2:// means the same. The official scheme has six query parameters: obfs, obfs-password, sni, insecure, pinSHA256 and ech. The port can be a list or range for port hopping. Most rejected links come down to an unencoded character, a port format the app does not read, or a certificate setting that does not match the server.

Tahir Mehmood, 9 min read

This is a field-by-field reference for Hysteria2 share links. For how the protocol works (QUIC, the /auth exchange, Brutal, Salamander and the server config), read [Hysteria2 protocol explained](https://tunnelhq.com/blog/hysteria2-protocol-explained/) first. This post only covers the link.

I checked everything against three sources: the URI scheme page in the official docs repository (HyNetworks/hysteria-website, last changed 22 August 2026), the client code of Hysteria 2.13.0 (5 October 2026), and the link parser in v2rayN 7.25.6, a widely used desktop client that imports these links. The old apernet/hysteria address now redirects to HyNetworks/hysteria.

## What does a hysteria2:// link look like?

It is a URL with the password in the user part, the server in the host part, and the TLS and obfuscation settings as query parameters:

```
hysteria2://[auth@]hostname[:port]/?[key=value]&[key=value]...[#name]
```

Here is a link that uses every standard parameter except `ech` (split over lines to read; a real link is one line):

```
hysteria2://s3cretPass@vpn.example.com:443/
  ?insecure=1
  &obfs=salamander
  &obfs-password=Ob5Passw0rd
  &pinSHA256=0f8a3c...e91b
  &sni=vpn.example.com
  #fra-01
```

And the shortest valid one. With no port it means 443, and with no `sni` the client uses the host name:

```
hy2://s3cretPass@vpn.example.com
```

The scheme can be `hysteria2` or `hy2`; they are the same. The official client's `hysteria share` command always writes `hysteria2://`, puts `/` before the `?`, and sorts the parameters alphabetically, so a link it generated looks like the first example.

There is also a separate `hysteria2+realm://` scheme for servers that use Realms. Its user part is the rendezvous server's token, not the password, and the password moves to an `auth` parameter. Do not mix the two up.

## What does each parameter do?

Each part of the link maps to one field of the client config. Everything else about the client stays local.

| Part | Required | Default | Format | Client config field |
| --- | --- | --- | --- | --- |
| Scheme | Yes |  | `hysteria2` or `hy2` |  |
| `auth` (before `@`) | No in the spec, yes in practice | Empty | Percent-encoded; `username:password` for `userpass` auth | `auth` |
| Host | Yes |  | Domain, IPv4, or IPv6 in brackets: `[2001:db8::1]` | `server` |
| Port | No | `443` | One port, or a multi-port list for port hopping | `server` |
| `obfs` | No | None | `salamander` or `gecko` | `obfs.type` |
| `obfs-password` | With `obfs` |  | Must match the server | `obfs.salamander.password` or `obfs.gecko.password` |
| `sni` | No | The host | Server name sent in TLS | `tls.sni` |
| `insecure` | No | `0` | `1` or `0` | `tls.insecure` |
| `pinSHA256` | No | None | SHA-256 of the server's leaf certificate, in hex | `tls.pinSHA256` |
| `ech` | No | None | ECH config list, base64; must match the server | `tls.ech` |
| `#name` | No |  | Display name only; carries no settings |  |

The spec is just as clear about what a link must not carry: bandwidth values and client modes (SOCKS5, HTTP, TUN and so on). Those depend on each user's network. The hop interval is not in the link either.

Apps may add their own parameters, and the spec allows it, but it also says no other app has to understand them. Two you will meet:

-   **v2rayN** reads `mport` for port-hopping ports, and `minPacketSize` and `maxPacketSize` for Gecko. Its own code notes that the packet size names are not in the official scheme and may change.
-   **TunnelHQ's checker** also accepts `peer` as another name for `sni`, and `obfspwd` for `obfs-password`.

## How are obfs, SNI and insecure written?

Write `obfs` with its type, `obfs-password` with the same password as the server, `sni` with the name on the certificate, and `insecure=1` only together with `pinSHA256`.

**Salamander:**

```
hysteria2://s3cretPass@vpn.example.com:443/?obfs=salamander&obfs-password=Ob5Passw0rd&sni=vpn.example.com
```

**Gecko** uses the same two parameters with `obfs=gecko`. An app that does not know Gecko yet will refuse the link, so check your users' clients before you switch.

**Self-signed certificate with a pin.** With `insecure=1` alone, the client accepts any certificate, so anyone between the user and the server could pose as it. With `pinSHA256` as well, the client still skips the normal check but accepts only the certificate with that exact hash:

```
hysteria2://s3cretPass@203.0.113.10:443/?insecure=1&pinSHA256=0f8a3c...e91b&sni=vpn.example.com
```

To get the hash, run this on the server against the certificate file:

```
openssl x509 -noout -fingerprint -sha256 -in /etc/hysteria/server.crt
```

It prints the hash with colons (`sha256 Fingerprint=0F:8A:3C:...`). You can paste it like that: Hysteria 2.13.0 lowercases the value and strips `:` and `-` before comparing. The pin is checked against the leaf certificate only, the first one the server sends, not a CA in the chain. So when the certificate is renewed, the pin changes and every old link fails.

v2rayN 7.25.6 treats any link that has a `pinSHA256` as insecure, even without `insecure=1`, because some providers issue links that way. Its code comment says this breaks compatibility between clients, and it does: the official client verifies the certificate normally unless `insecure=1` is set, so the same link can work in v2rayN and fail in the official client.

**Values with special characters.** The password and every value must be percent-encoded. In the query part, a `+` means a space, so an `obfs-password` that contains `+` has to be written `%2B`. The same goes for `&` (`%26`), `#` (`%23`) and `=` (`%3D`). In the password before the `@`, encode `@`, `/`, `?` and `#`.

`insecure` takes `1` or `0`. The official client also accepts `true` and `false`, because it parses the value as a Go boolean, but v2rayN only treats `1` as true. Write `1`.

## How are port-hopping ranges written in a link?

In the port position, after the host, using the same multi-port format as the client config: single ports, ranges, or both, separated by commas.

```
hysteria2://s3cretPass@vpn.example.com:20000-50000/?sni=vpn.example.com
hysteria2://s3cretPass@vpn.example.com:443,5000-6000,7044/?sni=vpn.example.com
```

The client picks a random port from the list for the first connection and moves to another one every 30 seconds by default. The interval is client config (`transport.udp.hopInterval`, at least 5 seconds), not part of the link. The server has to accept every port in the range, which the [protocol explainer](https://tunnelhq.com/blog/hysteria2-protocol-explained/) covers.

This format is the one place where apps differ the most:

-   **The official client** reads it. Go's standard URL parser rejects a port that is not a number, so Hysteria ships a modified copy of it just for this.
-   **v2rayN 7.25.6** reads hopping ports from the separate `mport` parameter instead, and writes them that way when it exports a link. The official client ignores `mport` and connects to the single port in the host part, without hopping.
-   **Generic URL libraries** in most languages fail on `host:20000-50000`, so a script or bot that parses links that way will choke on it.

If a link has to work in several apps, the safest form is one port in the host part. If the server accepts the whole range, any single port from it works.

## Why does a client reject my link?

Usually for one of six reasons: an unencoded character, a port format the app does not read, an obfuscation type it does not know, the wrong scheme, a certificate check that fails, or a password that does not match.

| What you see | Likely cause | Fix |
| --- | --- | --- |
| The app will not import the link at all | A raw `#`, `@`, `&` or space in the password, or a port range the app's parser cannot read | Percent-encode the password; use one port, or the app's own hopping field |
| `invalid config: server: ...` (official client) | The address could not be read; often a `#` in the password cut the link short, so the "host" is part of the password | Encode `#` as `%23` |
| `invalid config: obfs.type: unsupported obfuscation type` | `obfs` is not `salamander` or `gecko`, often a typo or an old value | Copy the type from the server config |
| The link starts with `hysteria://` | That is the Hysteria 1 format, which 2.x clients do not read; the two versions are not compatible | Get a `hysteria2://` link for a 2.x server |
| `no certificate matches the pinned hash` | The server's certificate changed, or the pin is for a CA, not the leaf | Recompute `pinSHA256` from the current certificate |
| A certificate error with no pin involved | Self-signed certificate without `insecure=1`, or `sni` does not match the certificate | Fix `sni`, or add a pin with `insecure=1` |
| `authentication error, HTTP status code: 404` | Wrong or badly encoded password, or the server uses `userpass` and the link has only one part | Check the encoding; use `username:password` |
| A timeout, with no error at all | Wrong `obfs-password`, or obfuscation set on only one side | Make the link match the server's `obfs` block |

The last row trips up the most people. A wrong obfuscation password does not cause an error, because the server cannot read the packets and drops them. To the client that looks exactly like a server that is down.

## How do I test a hysteria2 link works?

Load the link into a real client and send a request through it. Importing a link only proves it parses. Only a connection proves the server accepts it.

The official client can take a link as its whole server setting. Put the link in the `server` field and add a local proxy:

```yaml
server: hysteria2://s3cretPass@vpn.example.com:443/?sni=vpn.example.com
socks5:
  listen: 127.0.0.1:1080
```

Run `hysteria -c client.yaml`, wait for `connected to server`, and then in a second terminal:

```
curl --socks5-hostname 127.0.0.1:1080 -sS -o /dev/null -w '%{http_code}\n' https://www.gstatic.com/generate_204
```

`204` means the whole path works: the QUIC handshake, the password, obfuscation and the server's outbound connection. To get a clean link back from a working config, run `hysteria share -c client.yaml`, and add `--qr` to print it as a QR code for phones.

For a quick check without installing anything, paste the link into the [free TunnelHQ tester](https://tunnelhq.com/test/), five tests a day with no account. It reads `hy2://` and `hysteria2://` links with `sni` (or `peer`), `insecure`, `obfs` and `obfs-password`, then runs the official Hysteria client against the server. Two limits are worth knowing:

-   **It needs one explicit port in the link.** A link with no port or with a port range does not parse, and the result reads **Hysteria2 handshake failed** with `Config error: failed to parse Hysteria2 URI` under "What the checker saw". To test a port-hopping server, write one port from its range.
-   **It does not read `pinSHA256` or `ech`.** For a server with a self-signed certificate, the link needs `insecure=1` to pass, the same way the official client does without a pin.

When the link passes and you want to know the moment it stops working, [Hysteria2 monitoring](https://tunnelhq.com/hysteria2-monitoring/) runs the same check on a schedule and alerts you when it fails.

Source: https://tunnelhq.com/blog/hysteria2-link-format/
