Skip to main content

Request signing

Live keys require a signature by default. It proves the body was not altered in transit and bounds replay to five minutes.

The canonical string

{timestamp}.{METHOD}.{path-with-query}.{sha256hex(body)}
  • timestamp — Unix seconds (not milliseconds)
  • METHOD — uppercase verb
  • path-with-query — path including the query string, exactly as sent
  • sha256hex(body) — hex SHA-256 of the raw body; for no body, hash the empty string

Worked example — GET /v1/operations/lots?page=2, empty body, t=1735689600:

1735689600.GET./v1/operations/lots?page=2.e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

HMAC-SHA256 that with the signing secret, hex-encode, and send:

x-mt-timestamp: 1735689600
x-mt-signature: t=1735689600,v1=<hex>

The t= inside the signature must equal the x-mt-timestamp header. They are cross-checked so a fresh-looking header cannot wrap an old signed payload.

Examples

Pass signingSecret and it is handled per request, including on retries:

const client = new MineTech({
apiKey: process.env.MINETECH_API_KEY!,
signingSecret: process.env.MINETECH_SIGNING_SECRET!,
});

Troubleshooting

ErrorCause
signature_missingKey requires signing; headers absent
timestamp_missingx-mt-timestamp absent or not an integer
timestamp_mismatcht= disagrees with the header
timestamp_staleOutside the 300s window — check your clock
signature_mismatchDigest differs — see below

signature_mismatch is almost always one of:

  1. Milliseconds instead of seconds in the timestamp.
  2. A re-serialised body. Hash the exact bytes you transmit. If your HTTP client re-encodes JSON after you hash it, the digests differ.
  3. A path without its query string, or a URL-encoded difference between what you signed and what you sent.
  4. The API key used as the signing secret. They are two different values.

Verify your implementation against the worked example above — if it reproduces that exact digest with secret secret, your construction is right.