aisplice

Build it into your app

Request signing

#When you need it

An app without its own backend has to ship the key inside the app, where anyone capturing its traffic can read it. Turn on request signing on that key (Console → Keys → Edit → Request signing): every request must then also carry a signature made with a separate signing secret, so the sk- key alone is useless and a captured request cannot be replayed.

#The three headers

Keep sending Authorization as before and add three headers. The signed text is the timestamp, nonce, HTTP method and path (no query string) joined by newlines; the HMAC key is the sig_… secret exactly as shown in the console.

headers
X-Timestamp: 1759312345        # unix seconds, within 5 minutes of server time
X-Nonce:     9f2c4e7a1b3d5f60  # 16-128 chars of A-Z a-z 0-9 - _, new for every request
X-Signature: hex(HMAC-SHA256(secret, "1759312345\n9f2c4e7a1b3d5f60\nPOST\n/v1/images/tasks"))

#Sign a request

sign.dart
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';

const apiKey = 'sk-YOUR_KEY';
const signingSecret = 'sig_YOUR_SIGNING_SECRET';
int clockOffset = 0; // server time - device time, from X-Server-Timestamp

Map<String, String> signedHeaders(String method, String path) {
  final ts = (DateTime.now().millisecondsSinceEpoch ~/ 1000 + clockOffset).toString();
  final random = Random.secure();
  final nonce = List.generate(16, (_) => random.nextInt(256).toRadixString(16).padLeft(2, '0')).join();
  final signature = Hmac(sha256, utf8.encode(signingSecret))
      .convert(utf8.encode('$ts\n$nonce\n$method\n$path'))
      .toString();
  return {
    'Authorization': 'Bearer $apiKey',
    'X-Timestamp': ts,
    'X-Nonce': nonce,
    'X-Signature': signature,
  };
}

// headers: signedHeaders('POST', '/v1/images/tasks')
// GET /v1/images/tasks/{id} is signed with its own path, e.g. '/v1/images/tasks/task_123'
sign.mjs
import crypto from "node:crypto";

const ts = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString("hex");
const signature = crypto
  .createHmac("sha256", process.env.SIGNING_SECRET)
  .update(`${ts}\n${nonce}\nPOST\n/v1/images/tasks`)
  .digest("hex");

await fetch("https://www.aisplice.store/v1/images/tasks", {
  method: "POST",
  headers: {
    Authorization: "Bearer sk-YOUR_KEY",
    "Content-Type": "application/json",
    "X-Timestamp": ts,
    "X-Nonce": nonce,
    "X-Signature": signature,
  },
  body: JSON.stringify(payload),
});

#Errors and rules

  • Rejected requests answer 401 with error.code signature_required, signature_expired, signature_invalid or signature_replayed.
  • signature_expired also returns X-Server-Timestamp. Phones with a wrong clock should store the difference, add it to every timestamp, and retry once with a new nonce.
  • Retries need a new nonce and a new signature; a nonce is remembered for 10 minutes.
  • Replacing the secret in the console stops the old one within 30 seconds. Turning signing off and on again keeps the same secret.
  • Signing protects against captured traffic, not against someone who unpacks the app. Obfuscate the build (Flutter: --obfuscate) and keep a quota ceiling on the key.

Still stuck?

Send us the request ID or the error message and we will look into it.