Authentication
The two secrets
Almost every integration bug in this area comes from conflating these. They are unrelated.
The wallet password is not a setup-time secret you can forget about. It is required on every single transfer, so your system has to store it and retrieve it per user, for as long as the wallet exists.
Store the wallet password durably, per user, from the moment you call /create — and never lose it. A lost password means the user’s funds cannot be moved. Not by you, and not by us. The password is fixed for the life of the wallet, and the seed phrase from /create is the only other way to reach those keys — shown exactly once, at creation.
Getting a token
Your client id and api key go in headers. Send the request with an empty body.
You get two tokens. Present the token on every other endpoint as Authorization: Bearer <token>; keep the refreshToken to mint the next one. /auth and /refresh are the only endpoints that do not require a bearer token.
Token lifecycle
The access token lasts 15 minutes. The refreshToken lasts 7 days. When the access token expires, do not call /auth again — trade the refresh token at /refresh instead, so your api key stays off the wire after startup.
The response is the same shape as /auth: a new access token and a new refresh token.
Refresh tokens are single-use, and they rotate. Every /refresh consumes the token you presented and returns its replacement. Store the new one immediately — the one you just sent is dead.
Presenting an already-consumed token is what a stolen token looks like, so the service revokes the entire rotation chain: you are logged out too, and must go back to /auth with your api key. The practical consequence is that exactly one process may hold and roll a given refresh token. Two workers sharing one will race, one will replay, and both will be kicked out.
The practical pattern:
- Cache the access token until
expiresAtand reuse it. Do not mint one per request; it is a signing operation, not a free lookup. - Refresh slightly early (say, 60 seconds before
expiresAt) so a request in flight doesn’t expire mid-call. - On a
401, refresh once and replay the original request. If the refresh itself401s, fall back to/auth. If that401s, stop — you have a credential problem, not an expiry problem. - Call
/authonly at startup or after a refresh has been rejected.
401 means several different things
This is the one status code you must disambiguate by reading message, because the correct response to each is opposite.
A wrong wallet password is 401 Invalid credentials on every chain. Omitting password entirely on a Bitcoin /send is the one variation: that is a 400 Missing/Invalid password.
A 401 on /send is the one failure you can trust completely. The password is checked before anything is signed, so nothing was broadcast. Once you fix the password, retrying is entirely safe — no risk of a double-send. Contrast that with a 500 or a timeout, which is genuinely ambiguous — see Reliability.
X-USER-ID overrides the body
X-USER-ID is an optional request header. When it is present, it overrides the userId in your request body — the body value is ignored entirely.
- When it is absent, the body
userIdis used. That is what these docs assume, and why every example putsuserIdin the body. - When it is present, that id wins, whatever the body says.
This will surprise you if you don’t know about it: your body is not silently ignored because it is malformed, but because a header outranked it. If your calls seem to operate on the wrong user, check whether an X-USER-ID is being attached to them somewhere along the way.
Throttling
Throttle yourself. Since you learn a wallet’s state by polling for it (see Reliability), a naive integration checks every user against every asset on a tight loop and generates enormous load for no benefit. Poll only the assets you actually support, stagger users across the interval, and back off when nothing is changing.
Your operator sets the request limits that apply to you — ask them what yours are before you design a polling schedule.
Credential issuance
Your operator issues the client id and api key, and shows them once — only a SHA-256 hash of the key is stored, so it cannot be recovered or re-displayed. Lost credentials are re-issued, not recovered. To rotate credentials, ask your operator.
You need a separate set per environment. Sandbox (https://api-demo.walletstech.com) and production (https://api.walletstech.com) do not share credentials — a sandbox client id and api key will not authenticate against production, and presenting them there is a 401 Invalid credentials, not a hint that something is misconfigured. Request sandbox credentials first, build against sandbox, then ask for production credentials when you are ready to go live.
If /auth or /refresh returns 403, your credentials are valid but the api client is not fully provisioned, so no token can be issued. This is not something you can fix from the client side — retrying, or falling back from /refresh to /auth, returns the same 403. Ask your operator to finish provisioning it.