API reference
Authentication
Reads are public. Anything for one wallet needs a wallet sign in.
Who needs it
Sign in, step by step
- 1Ask for a challenge with the wallet address. You get a message and a token.
- 2Sign the message bytes with the wallet (an ed25519 signMessage). Signing moves nothing.
- 3Send the wallet, the base58 signature and the token back. The answer sets the session cookie.
- 4Send the cookie with every request that needs a session.
- A challenge works once and expires after 5 minutes. The server checks its own copy of the message, so the browser cannot change what was signed.
- A session lasts 7 days.
Ask for a challenge
/api/authNo sign in, CSRF headerStarts a sign in. Answers plain JSON (no envelope).
- Sign in
- Not needed
- CSRF header
- Needed:
x-zpad: zpad - Cached
- Never
- Rate limit
- 20 an hour per address (IP)
Body
Response
{
"message": "<the message to sign>",
"token": "<challenge token>"
}Errors
Example
curl -s -X POST "/api/auth" \
-H "x-zpad: zpad" \
-H "content-type: application/json" \
-d '{"step":"challenge","wallet":"<wallet address>"}'Verify the signature
/api/authNo sign in, CSRF headerFinishes a sign in and sets the session cookie. Answers plain JSON.
- Sign in
- Not needed
- CSRF header
- Needed:
x-zpad: zpad - Cached
- Never
- Rate limit
- None of its own
Body
Response
{
"wallet": "<wallet address>"
}Errors
Example
curl -s -X POST -c cookies.txt "/api/auth" \
-H "x-zpad: zpad" \
-H "content-type: application/json" \
-d '{"step":"verify","wallet":"<wallet address>","signature":"<base58 signature>","token":"<challenge token>"}'In the browser
The same steps in a page, with the wallet's own signMessage. bs58 is the base58 package.
const headers = { "content-type": "application/json", "x-zpad": "zpad" };
const post = (body) => fetch("/api/auth", { method: "POST", headers, body: JSON.stringify(body) }).then((r) => r.json());
const { message, token } = await post({ step: "challenge", wallet });
const signed = await signMessage(new TextEncoder().encode(message)); // your wallet's signMessage
const { wallet: signedIn } = await post({ step: "verify", wallet, token, signature: bs58.encode(signed) });Sign out
/api/authNo sign in, CSRF headerEnds the session and clears the cookie.
- Sign in
- Not needed
- CSRF header
- Needed:
x-zpad: zpad - Cached
- Never
- Rate limit
- None of its own
Body
Response
{
"ok": true
}Example
curl -s -X POST -b cookies.txt "/api/auth" \
-H "x-zpad: zpad" \
-H "content-type: application/json" \
-d '{"step":"logout"}'Who is signed in
/api/auth/meNo sign inThe wallet of the current session, or null. GET /api/auth answers the same.
- Sign in
- Not needed
- CSRF header
- Not needed
- Cached
- Never
- Rate limit
- None of its own
Response
{
"wallet": "<wallet address>" | null
}Example
curl -s -b cookies.txt "/api/auth/me"The session cookie
With curl, save the cookie with -c cookies.txt when you verify, and send it back with -b cookies.txt, as the examples in these docs do.
The CSRF header
Every POST and PATCH needs the header x-zpad: zpad. Browsers cannot send it from another site without asking first, which the API never allows. A request whose Origin is another site is refused with 403.
Requests without an Origin header, like curl, pass on the header alone.