Skip to main content

Authentication & Request Signing

Read this entire page before you write a single line of integration code. Every authenticated endpoint requires a per-request cryptographic signature. Get this wrong and every call returns 401 Unauthorized regardless of what else you do. ⚠️ If you are arriving here from the older Postman documentation, please discard it. It used the wrong algorithm. This document reflects the actual deployed behaviour as of 10 May 2026, verified live against production.

TL;DR

  1. The Dr Green API authenticates each request with ECDSA (curve secp256k1) over SHA-256.
  2. Your apiKey is the Base64-encoded PEM public key (SPKI). Your secretKey is the Base64-encoded PEM private key (PKCS8). They come as a pair from POST /keys.
  3. Every authenticated request sends two headers:
    • x-auth-apikey: <Base64 of PEM public key>
    • x-auth-signature: <Base64 of ECDSA-SHA256 signature over the canonical payload>
  4. The canonical payload depends on HTTP method (see § Canonical payload below). Sign and send the same exact string.
  5. Treat secretKey like a database password — there’s no replay protection, so anyone with it can sign as you.

The cryptographic primitive

This is the same primitive Bitcoin and Ethereum use for transaction signatures. Every mainstream language has good library support. See § Implementation by language for working code.

How keys are issued

Holders authenticate to the DAPP UI via wallet sign-in (SIWE-style):
Stores never see the holder’s wallet or JWT. They get a long-lived (apiKey, secretKey) pair the holder generated on their behalf in the DAPP UI. A holder can issue up to 100 key pairs and revoke any of them via PATCH /keys/delete at any time.

Canonical payload

⚠️ The most common cause of 401 errors during integration is signing the wrong payload. Read this section twice.
The server reproduces a “canonical payload” from your incoming request and verifies your signature against it. You must sign the byte-for-byte identical string the server will reproduce. The reproduction rules vary by HTTP method:
⚠️ Important correction (10 May 2026): earlier drafts of this doc said the empty-GET case signs "" (empty string). It does not. Verified live: empty-query GETs sign "{}". Signing "" produces 401 "User is not authorized". This applies to routes both with and without path parameters.

Examples

Critical rules

  1. Sign the exact string you’ll send. If you JSON.stringify twice, you may get different output (object key ordering varies between calls). Stringify once, sign that string, send that string.
  2. No whitespace in JSON. JSON.stringify(obj) with no space arg gives compact form. In Python: json.dumps(obj, separators=(",", ":")). In PHP: json_encode($obj).
  3. Query params ordered as sent. The server uses Express’s req.query, which preserves insertion order. If you sort keys differently when signing vs. sending, you’ll fail.
  4. Send Content-Type: application/json only when you have a body. Setting it on a GET (with no body) doesn’t break things, but it’s not necessary; setting it on a GET with the canonical-payload-string-as-body will return 400 because Express’s body-parser will try to JSON.parse it.

The wire format

A complete authenticated request looks like this:
For a POST:
Note the body in the POST is byte-identical to the canonical payload. That’s not a coincidence — the canonical-payload-for-POST rule and the actual body are the same string.

The three-layer auth flow

The Dr Green backend uses three different auth strategies depending on the route:
⚠️ JWT-only routes you cannot reach from a store integration:
  • GET /api/v1/user/me
  • GET /api/v1/dapp/users/nfts
These return 401 "Unauthorized" (the short-form message) regardless of how correct your API-key signature is. As a store builder, you need to ask the holder to set their primary NFT in the DAPP UI before they hand you keys — you can’t list their NFTs from your side.
If your request is rejected with 401 "User is not authorized" (the longer-form message), the DAPP guard is firing — your API key is being recognised but your signature doesn’t match. Re-check your canonical payload.

Implementation by language

Helper code is in /examples/<lang>/. Below is the minimal signing function for each language.

Node.js / TypeScript

Python

cURL (bash)

PHP


Verifying your signature locally

Before you fire requests against the live API, verify your signing works locally:
If Verified OK prints locally but the API still returns 401, the bug is in your canonical-payload reproduction, not your crypto.

Operational hygiene

  • Don’t commit secretKey to git. Use .env and add it to .gitignore. Use a secrets manager (AWS Secrets Manager, GCP Secret Manager, Vault) in production.
  • Don’t log signatures. They’re deterministic-ish (ECDSA randomises but with the key compromise risk is the same as logging the key).
  • Rotate keys when staff leave. A holder issues up to 100 key pairs; deactivate and re-issue when needed via PATCH /keys/delete and POST /keys.
  • There is no replay protection. Anyone with your secretKey can sign valid requests indefinitely. Treat it as a database-tier secret and confirm with Dr Green if/when timestamp-based replay protection ships.

Common 401 causes (the diagnostic table)