跳到主要内容

Signed Request Walkthrough

This page shows a complete signed request end-to-end for each of BTSE's URL shapes. Every part of the request is explicit — wire URL, signed payload, headers, body, and a runnable cURL — in one place, on one screen.

If you've read Authentication and are still getting 401 Authentication Failed or api parameter is mandatory, the walkthroughs below are the canonical reference.

Computed signatures are reproducible

The signatures shown below were generated with the demo API key, secret, and nonce in each section. Plug those exact values into your code and you should compute the same request-sign — if you don't, your signer disagrees with ours. Use the Auth Tester to verify in the browser.


What you sign

The signing algorithm is identical for every endpoint, but the base URL and the signed urlpath differ per surface:

SurfaceProductsBase URL (= spec server)Signed urlpath (= spec path)
Spot v4Spothttps://api.btse.com/spot/api/v4/...
Futures v3Futureshttps://api.btse.com/futures/api/v3/...
/spot prefixEarnhttps://api.btse.com/spot/api/v3.3/invest/...
public-apiWallet, OTC, Marketshttps://api.btse.com/public-api/... (full path)

The key rule: what you sign is the path exactly as it appears in the OpenAPI spec for that endpoint — i.e., everything between the host and the query string. Whatever the spec places in the server URL (e.g. /futures/api for Futures) is part of the host, not part of the signed path. For Spot v4 the /spot prefix is part of the spec path, so it is signed.

Always signedNever signed
The path from the OpenAPI spec (e.g. /spot/api/v4/trade/orders)Any prefix that lives in the server URL (/futures/api, /spot for Earn)
The current nonce in millisecondsQuery string (even when present on the wire)
The raw request body for POST/PUT/DELETEHeader values

Example 1 — Spot signed GET with a query string

Endpoint: GET /spot/api/v4/trade/orders — list the caller's open orders, optionally filtered by symbol.

Demo inputs (use these to reproduce the signature in your own code):

FieldValue
API key4e9536c79f0fdd72bf04f2430982d3f61d9d76c996f0175bbba470d69d59816x
Secret848db84ac252b6726e5f6e7a711d9c96d9fd77d020151b45839a5b59c37203bx
Nonce (ms)1715000000000
MethodGET
Wire URLhttps://api.btse.com/spot/api/v4/trade/orders?symbol=BTC-USD
Signed urlpath/spot/api/v4/trade/orders
Body string(empty)

String to sign (concatenated, no separators):

/spot/api/v4/trade/orders1715000000000

Resulting request-sign:

b50e5f80f6a5bec9185fb6dac72bad3ec5b27775b8da2f3c5f9c52b1b393ced5772f06228f82c76b6e7b66ad8ace8651

Wire request:

curl -X GET 'https://api.btse.com/spot/api/v4/trade/orders?symbol=BTC-USD' \
-H 'request-api: 4e9536c79f0fdd72bf04f2430982d3f61d9d76c996f0175bbba470d69d59816x' \
-H 'request-nonce: 1715000000000' \
-H 'request-sign: b50e5f80f6a5bec9185fb6dac72bad3ec5b27775b8da2f3c5f9c52b1b393ced5772f06228f82c76b6e7b66ad8ace8651'

Why the query string is missing from the signed payload: the server reconstructs urlpath from the request without the ?... portion. If you include ?symbol=BTC-USD in the string you sign, the server computes a different signature and returns 401 Authentication Failed. Send the query string on the wire, but do not sign it.


Example 2 — Futures signed POST with a JSON body

Endpoint: POST /v3/trade/orders — place a futures order.

Demo inputs:

FieldValue
API key4e9536c79f0fdd72bf04f2430982d3f61d9d76c996f0175bbba470d69d59816x
Secret848db84ac252b6726e5f6e7a711d9c96d9fd77d020151b45839a5b59c37203bx
Nonce (ms)1715000000000
MethodPOST
Wire URLhttps://api.btse.com/futures/api/v3/trade/orders
Signed urlpath/v3/trade/orders
Body{"symbol":"BTC-PERP","orderType":"LIMIT","orderSide":"BUY","orderSize":1,"orderPrice":57009.5}

String to sign:

/v3/trade/orders1715000000000{"symbol":"BTC-PERP","orderType":"LIMIT","orderSide":"BUY","orderSize":1,"orderPrice":57009.5}

Resulting request-sign:

ce94311ce48fd256e0f724377b24b16a222d7fbbd282bdc933ecfeda5d0a2c87ca2dba26a193ca5281312a231e021038

Wire request:

curl -X POST 'https://api.btse.com/futures/api/v3/trade/orders' \
-H 'request-api: 4e9536c79f0fdd72bf04f2430982d3f61d9d76c996f0175bbba470d69d59816x' \
-H 'request-nonce: 1715000000000' \
-H 'request-sign: ce94311ce48fd256e0f724377b24b16a222d7fbbd282bdc933ecfeda5d0a2c87ca2dba26a193ca5281312a231e021038' \
-H 'Content-Type: application/json' \
-d '{"symbol":"BTC-PERP","orderType":"LIMIT","orderSide":"BUY","orderSize":1,"orderPrice":57009.5}'

The body you sign must be byte-identical to the body you send. If your HTTP client reorders keys, adds whitespace, or re-stringifies the JSON differently between signing and sending, the signature will not match. Serialize once, sign that string, send that string.

Note the Futures base URL is https://api.btse.com/futures/api — the /futures/api prefix lives in the host, so the signed urlpath starts at /v3/....


Example 3 — Wallet signed GET on public-api

Endpoint: GET /public-api/wallet/v1/user/assets — list wallet assets.

Demo inputs:

FieldValue
API key4e9536c79f0fdd72bf04f2430982d3f61d9d76c996f0175bbba470d69d59816x
Secret848db84ac252b6726e5f6e7a711d9c96d9fd77d020151b45839a5b59c37203bx
Nonce (ms)1715000000000
MethodGET
Wire URLhttps://api.btse.com/public-api/wallet/v1/user/assets
Signed urlpath/public-api/wallet/v1/user/assets
Body string(empty)

String to sign:

/public-api/wallet/v1/user/assets1715000000000

Resulting request-sign:

f1a04a8842726e92b4d4bf915b485b818ad707ac23014fa7eddbce5ac2c4ac3288f9491275a378c112376919c03c5309

Wire request:

curl -X GET 'https://api.btse.com/public-api/wallet/v1/user/assets' \
-H 'request-api: 4e9536c79f0fdd72bf04f2430982d3f61d9d76c996f0175bbba470d69d59816x' \
-H 'request-nonce: 1715000000000' \
-H 'request-sign: f1a04a8842726e92b4d4bf915b485b818ad707ac23014fa7eddbce5ac2c4ac3288f9491275a378c112376919c03c5309'

The whole /public-api/... path is signed — there is no separate product prefix to strip, because the host is just https://api.btse.com. This is the difference between public-api endpoints (Wallet, OTC, Markets) and Futures (/futures/api prefix in the host).


Verifying your signer against these examples

Drop the inputs from any example above into your signer and compute HMAC-SHA384(secret, urlpath + nonce + bodyStr) — you should get the same request-sign shown.

If they don't match, the most likely causes (in order):

  1. You're hashing the wrong string. Print the exact bytes your code passes to HMAC and compare character-by-character with the "String to sign" block.
  2. You included the query string (Example 1 is the canonical test for this).
  3. Your body string differs from the one you signed (Example 2 is the canonical test for this).
  4. Wrong algorithm. It is HMAC-SHA384, hex-encoded — not SHA256, not SHA512, not base64.
  5. Wrong header names. Use request-api, request-nonce, request-sign — not X-BTSE-*, BTSE-API-KEY, etc. Wrong header names typically surface as api parameter is mandatory, not a signature error.

See Error Codes → Common error strings for a fuller lookup of opaque error messages to root causes.


See also

  • Authentication — full reference: headers, permissions, rate limits, multi-language signer code
  • Auth Tester — interactive signature tool and live request runner
  • Error Codes — HTTP and API status code reference
  • Machine-readable specs — OpenAPI and AsyncAPI files for code generation and agent consumption