# xhshow: a Python library that reproduces Xiaohongshu request signatures

> xhshow is a MIT-licensed Python package that computes the `x-s`, `x-s-common` and `x-rap-param` request headers Xiaohongshu's web API expects, so a script can call the same endpoints a browser would. The judgement is that it is a competent single-dependency reimplementation with a genuinely careful API surface, and that its value evaporates the moment the platform changes the algorithm it reverses.

**Cloxl/xhshow** — 小红书xs纯算 小红书x-s x-s-common xsc 等字段 纯算逆向

- Repository: https://github.com/Cloxl/xhshow
- Stars: 1,098 · Forks: 271
- Language: Python
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/cloxl-xhshow

## A signing library, not a scraper

xhshow generates the request headers that Xiaohongshu's web API requires, specifically `x-s`, `x-s-common` and `x-rap-param`. The project describes itself as a reverse engineering of these fields, computed purely in Python rather than by driving a real browser.

The scope is deliberately narrow. The package does not log in for you, does not create accounts, and does not fetch content on its own. It signs requests. You supply the session cookies, and it hands back a set of headers you pass to your own HTTP client. The only runtime dependency is `pycryptodome>=3.23.0`, and the project requires Python 3.10 or newer, with the classifiers listing 3.10, 3.11 and 3.12.

The library lives on PyPI as `xhshow`, and installation is the ordinary pip invocation:

```bash
pip install xhshow
```

The public surface is the `Xhshow` class. The two entry points you will actually use are `sign_headers_get` and `sign_headers_post`, and both take a URI, the cookies, and either `params` or `payload`. The URI argument is flexible: you may pass a full URL or a bare URI path, and the library extracts what it needs from it. That is a small design decision with real value, because the signing algorithm mixes the path into its input and a full URL and a path are not interchangeable to it.

The build is handled by `hatchling` with `hatch-vcs`, so the version is derived from the version control tags and written to `src/xhshow/_version.py` at build time. The package metadata lists the project as `Development Status :: 4 - Beta` and classifies it under `Topic :: Security :: Cryptography`, and it ships type information since the package is marked as typed.

## The headers it returns and where each one comes from

A signed GET returns a set of headers that map fairly directly onto the platform's own request format:

```python
{
    "x-s": "XYS_...",
    "x-s-common": "...",
    "x-t": "1234567890",
    "x-b3-traceid": "...",
    "x-xray-traceid": "...",
    "x-mns": "unload",
    "xy-direction": "42",
}
```

Each field has a separate generator, and the library exposes them individually as well as through the combined call. `x-s` is the actual signature and needs only the `a1` cookie value. `x-s-common` needs the complete cookie set and accepts either a dictionary or a raw string. `x-t` is a millisecond timestamp. `x-b3-traceid` is a 16 character trace identifier and `x-xray-traceid` is a 32 character one.

What matters here is the timestamp consistency. The documentation shows a pattern where a single `time.time()` value is captured and then passed explicitly into `sign_xs_get`, `get_x_t` and `get_xray_trace_id`, with the latter converting to milliseconds. The comment in the code describes this as ensuring the timestamps in each field are consistent. If the signature algorithm mixes a timestamp into its input and the header carries another, a mismatch would produce a signature that does not validate, and the library makes that failure mode avoidable rather than leaving it to chance.

Two other helper methods are worth noting because they let you skip the browser entirely. `Xhshow.generate_a1()` produces an `a1` cookie value of 52 characters, and `Xhshow.generate_web_id(a1)` derives a 32 character hex `web_id` from it. There is also a `client.build_url` method that constructs a GET link matching the platform's expectations, and `client.build_json_body` for a POST body.

## x-rap-param is the risk-control header and it is optional

Some endpoints require an extra header called `x-rap-param`, described as a risk-control header needed by feed, search and note publishing interfaces. You request it with a single flag:

```python
headers = client.sign_headers_post(
    uri="https://edith.xiaohongshu.com/api/sns/web/v1/feed",
    cookies=cookies,
    payload={"source_note_id": "..."},
    x_rap=True,
    user_id="5ff...",
)
```

The `x_rap` value is computed from the request API path plus the body, and the documentation states that both GET and POST are supported. The underlying function is also importable on its own from `xhshow.core.xrap` as `x_rap_param`, taking a path prefixed with `//` and the body dictionary.

The `user_id` argument controls something more interesting. When you pass a `user_id`, the `xy-direction` value is computed from it with MurmurHash3. When you omit it, the value is random. Since `xy-direction` is a sharding indicator, a request that carries a random value may be routed differently from one that carries the value consistent with the account. For a script that makes many requests under one session, that inconsistency is the kind of thing that produces intermittent behaviour that is hard to diagnose from the outside, and passing the user id removes the variable.

The library also exposes two decode helpers, `client.decode_x3` for a signature beginning `mns0101_` and `client.decode_xs` for a full `XYS_` signature. These are the inverse operations, useful for inspecting how a captured request was signed.

## SessionManager trades randomness for continuity

The experimental part of the library is `SessionManager`, and the documentation is honest about its status.

A `SessionManager` holds stateful signing parameters, specifically a fixed page load timestamp and a monotonically increasing counter. The stated purpose is to simulate continuous real user operation, with a possible improvement in long-term stability. The basis is a theoretical analysis attached to issue #86, and the documentation says explicitly that the actual effect remains to be verified. That is a useful signal: the author is presenting a hypothesis rather than a measurement, and the phrase "may improve" is doing real work in that sentence.

The usage is straightforward. You create a `SessionManager`, pass it into `sign_headers_get` as the `session` argument, and reuse the same session object across multiple requests. For multiple accounts, the guidance is to create a separate `SessionManager` per account and match them per account. A shared session across accounts would mean sharing the same page load timestamp and the same counter, which defeats the point of the mechanism.

There is a `search` and `request_id` pair for the search endpoints. `client.get_search_id()` returns a base36 value, and `client.get_search_request_id()` returns a string of the form `{random}-{ts_ms}`. The timestamp in milliseconds again means the same clock as the `x-t` header, so keeping them aligned is a matter of passing the same value through, the same pattern the individual field generators support.

## CryptoConfig lets you override the constants the algorithm depends on

A library that reimplements a private algorithm needs a way to adjust when the platform's constants change, and `CryptoConfig` is that mechanism. It is importable from the package root alongside `Xhshow`:

```python
from xhshow import CryptoConfig, Xhshow

config = CryptoConfig().with_overrides(
    X3_PREFIX="custom_",
    SEQUENCE_VALUE_MIN=20,
    SEQUENCE_VALUE_MAX=60,
)
client = Xhshow(config=config)
```

The override surface includes `X3_PREFIX` and the bounds of the sequence value, which is exactly the kind of constant that a platform change would touch. The defaults are baked in, and the override path is designed so that a change of this kind does not require patching the library internals.

Whether that is enough depends on how the platform actually changes things, and the library cannot promise it. A signature algorithm is not a constant table. If the platform alters the structure of the computation rather than a value inside it, a config override will not save you, and a new release of the library will be needed. The MIT licence and the small single-dependency footprint do at least make forking and patching a realistic fallback.

For a consumer, the practical reading is that `CryptoConfig` is the first thing to reach for when a previously working request starts failing, and the release history suggests the maintainer does adjust it, with v0.1.8 on 2026-01-15, v0.1.9 on 2026-02-17 and v0.2.0 on 2026-06-11. The last push to the repository was on 2026-06-11, the same day as the v0.2.0 release.

## Conventions, tooling and the community it comes from

The development setup is modern and conventional. The project uses `uv` for environment management, and the documented workflow is:

```bash
git clone https://github.com/Cloxl/xhshow && cd xhshow
uv sync --dev

uv run pytest tests/ -v
uv run ruff check src/ tests/ --ignore=UP036,E501
uv run ruff format src/ tests/
uv build
```

The lint configuration is detailed. Ruff selects the pycodestyle error and warning rules, pyflakes, isort, flake8-bugbear, flake8-comprehensions and pyupgrade sets, targets `py310`, and uses a line length of 120. Per-file ignores allow asserts in tests and relax rules on the auto-generated `src/xhshow/_version.py`. Black is also a declared dev dependency with a line length of 120, and pytest is configured with `testpaths` set to `tests`, `pythonpath` including `src`, and `asyncio_mode` set to `auto`. Commits follow the conventional commits specification, while the version itself is derived from version control tags by `hatch-vcs`.

Two things about the project's social context are worth stating. The README says the project is shared on LINUX DO, described as a technical community, and it points to issue #60 as the place for feature suggestions. That tells you the audience is a Chinese-speaking developer community focused on this kind of API work.

The licence is MIT, with the `LICENSE` file in the repository, and the package metadata records the classifier `License:: OSI Approved:: MIT License`. The `pyproject.toml` is the single source of truth for dependency and tool versions, and the repository has a `tests/` directory, a `src/` layout, a `uv.lock` for reproducible environments, and `.github/` and `.claude/` directories at the root.

One boundary deserves naming. This library computes signatures for a third-party platform's private API. Using it means making requests the platform did not design for scripted use, which is a matter for the platform's terms of service and your own judgement rather than for the library. The MIT licence covers the code, and it does not grant any right to use Xiaohongshu's API.

## Conclusion

xhshow fits a Python developer who has already obtained valid Xiaohongshu session cookies and needs to script against the web API rather than scrape it by driving a browser, and who accepts that the platform's terms and its changing signing scheme are the real constraints. It does not fit someone who needs account creation, and it does not fit a production service that needs a stability guarantee, because the library mirrors a private algorithm that the platform can change without notice and the repository has moved from v0.1.8 through v0.1.9 to v0.2.0 with the last push on 2026-06-11. Verify first that your `a1`, `web_session` and `webId` cookie values are accepted, that the signed request returns real data rather than an empty body, and that the `xy-direction` value your requests carry matches the account, since the documentation is explicit that passing `user_id` computes it with MurmurHash3 and omitting it produces a random value.

## FAQ

### How do I install and use xhshow to sign Xiaohongshu requests?

Install it with `pip install xhshow`, then create an `Xhshow` instance and call `sign_headers_get` or `sign_headers_post` with a URI, your cookies, and either `params` or `payload`. The returned dictionary contains the `x-s`, `x-s-common` and other headers to pass to your HTTP client.

### What does xhshow need for the x-rap-param risk control header?

Pass `x_rap=True` to the signing call. The value is computed from the request API path and the body, for both GET and POST, and the underlying `x_rap_param` function can also be imported from `xhshow.core.xrap`.

### Why does xhshow use MurmurHash3 for the xy-direction value?

When you pass a `user_id` to the signing call, the `xy-direction` value is computed from it with MurmurHash3, and when you omit it the value is random. Passing the user id keeps the sharding value consistent with the account across requests.

### Is the xhshow SessionManager feature proven to work?

The documentation describes it as experimental. It maintains a fixed page load timestamp and a monotonically increasing counter to simulate continuous user operation, based on a theoretical analysis in issue #86, and the documentation states that the actual effect on long-term stability remains to be verified.

### How do I change xhshow's crypto constants when they change upstream?

Use `CryptoConfig().with_overrides(...)` to override values such as `X3_PREFIX`, `SEQUENCE_VALUE_MIN` and `SEQUENCE_VALUE_MAX`, then pass the config to `Xhshow(config=config)`. Structural changes to the platform's algorithm would still require a new library release or a patch.

## Sources

- [Cloxl/xhshow on GitHub](https://github.com/Cloxl/xhshow)
- [Issues](https://github.com/Cloxl/xhshow/issues)
- [License: MIT](https://github.com/Cloxl/xhshow/blob/master/LICENSE)
- [README](https://github.com/Cloxl/xhshow/blob/master/README.md)
- [Releases](https://github.com/Cloxl/xhshow/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/cloxl-xhshow
