Developer API reference

Scan. Verify. Trust. — server-to-server product verification for your backend.

Overview

The Developer API lets your backend check whether a product's QR code is genuine, without a user session or the Qrauthix mobile app — authenticated with the API application credentials you manage in this portal under Applications.

Base URL (production): https://api.qrauthix.com. In this local development environment the same endpoint is served by the Qrauthix.Api project, typically at https://localhost:7077.

Authentication

Every Developer API request must include the client ID/secret pair for one of your API credentials, sent as two headers:

X-Client-Id: qrx_<your client id>
X-Client-Secret: <your client secret>

Credentials are scoped to one API application and organization. Rotating a credential immediately invalidates its previous secret; revoking one disables it permanently. Store the secret server-side only — it is shown exactly once, at creation or rotation time, and is never retrievable again.

Verify a QR code

POST /api/v1/developer/verify

Request body:

{
  "qrCode": "QRX-AB12CD34"
}

Success response — 200 OK:

{
  "success": true,
  "verified": true,
  "verificationType": "Simple",
  "product": {
    "name": "Aurora 40W Charger",
    "brand": "Aurora Electronics",
    "batch": "BATCH-2026-014"
  },
  "verification": {
    "verifiedAt": "2026-09-11T10:15:00Z",
    "verificationId": "5b1f8c2a-9e0a-4b8e-9a2d-3a6f2e7c9d10"
  }
}

product and verification are null when verified is false (for example, an unknown or inactive QR code) — check verified, not just the HTTP status, since a "not verified" result for a well-formed request is still a 200 OK. Malformed requests or unknown/revoked credentials return the error envelope described below instead.

Code samples

curl -X POST https://api.qrauthix.com/api/v1/developer/verify \
  -H "X-Client-Id: qrx_your_client_id" \
  -H "X-Client-Secret: your_client_secret" \
  -H "Content-Type: application/json" \
  -d '{"qrCode": "QRX-AB12CD34"}'
using var client = new HttpClient { BaseAddress = new Uri("https://api.qrauthix.com") };
client.DefaultRequestHeaders.Add("X-Client-Id", clientId);
client.DefaultRequestHeaders.Add("X-Client-Secret", clientSecret);

var response = await client.PostAsJsonAsync("/api/v1/developer/verify", new { qrCode = "QRX-AB12CD34" });

if (!response.IsSuccessStatusCode)
{
    var error = await response.Content.ReadFromJsonAsync<ApiErrorResponse>();
    throw new InvalidOperationException($"{error?.Error.Code}: {error?.Error.Message}");
}

var result = await response.Content.ReadFromJsonAsync<VerificationResponse>();
Console.WriteLine($"Verified: {result?.Verified}, product: {result?.Product?.Name}");

public sealed record ProductSummary(string Name, string? Brand, string? Batch);
public sealed record VerificationInfo(DateTime VerifiedAt, Guid VerificationId);
public sealed record VerificationResponse(bool Success, bool Verified, string VerificationType, ProductSummary? Product, VerificationInfo? Verification);
public sealed record ApiError(string Code, string Message, string? RequestId);
public sealed record ApiErrorResponse(bool Success, ApiError Error);
// Server-side only (Node.js / your backend) — never ship the client secret to a browser.
const response = await fetch("https://api.qrauthix.com/api/v1/developer/verify", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Client-Id": process.env.QRAUTHIX_CLIENT_ID,
    "X-Client-Secret": process.env.QRAUTHIX_CLIENT_SECRET
  },
  body: JSON.stringify({ qrCode: "QRX-AB12CD34" })
});

const payload = await response.json();
if (!response.ok) {
  throw new Error(`${payload.error.code}: ${payload.error.message}`);
}

console.log(payload.verified, payload.product?.name);
val client = OkHttpClient()
val mediaType = "application/json".toMediaType()
val body = """{"qrCode":"QRX-AB12CD34"}""".toRequestBody(mediaType)

val request = Request.Builder()
    .url("https://api.qrauthix.com/api/v1/developer/verify")
    .addHeader("X-Client-Id", clientId)
    .addHeader("X-Client-Secret", clientSecret)
    .post(body)
    .build()

client.newCall(request).execute().use { response ->
    val text = response.body?.string().orEmpty()
    if (!response.isSuccessful) {
        throw IOException("Verify failed (${response.code}): $text")
    }
    println(text) // parse with kotlinx.serialization / Moshi / Gson as usual
}
var request = URLRequest(url: URL(string: "https://api.qrauthix.com/api/v1/developer/verify")!)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(clientId, forHTTPHeaderField: "X-Client-Id")
request.setValue(clientSecret, forHTTPHeaderField: "X-Client-Secret")
request.httpBody = try JSONEncoder().encode(["qrCode": "QRX-AB12CD34"])

let (data, response) = try await URLSession.shared.data(for: request)
guard let http = response as? HTTPURLResponse else { throw URLError(.badServerResponse) }

if !(200..<300).contains(http.statusCode) {
    struct ApiError: Decodable { let code: String; let message: String }
    struct ApiErrorResponse: Decodable { let success: Bool; let error: ApiError }
    let err = try JSONDecoder().decode(ApiErrorResponse.self, from: data)
    throw NSError(domain: err.error.code, code: http.statusCode)
}

struct ProductSummary: Decodable { let name: String; let brand: String?; let batch: String? }
struct VerificationResponse: Decodable { let success: Bool; let verified: Bool; let verificationType: String; let product: ProductSummary? }
let result = try JSONDecoder().decode(VerificationResponse.self, from: data)
print(result.verified, result.product?.name ?? "")

Response & error envelope

Successful responses return the resource directly (as shown above). Every error — validation, authentication, not-found, conflict, rate limit, or unexpected server error — returns the same JSON shape, with an HTTP status code that matches the error:

{
  "success": false,
  "error": {
    "code": "INVALID_CREDENTIALS",
    "message": "Invalid email or password.",
    "requestId": "3f2c9b8a7d6e4c1b9a0f8e7d6c5b4a39"
  }
}

Always branch on error.code (a stable machine-readable string), not on error.message (a human-readable string that may change).

Error codes

CodeTypical statusMeaning
AUTH_REQUIRED 401 No credentials were supplied on a request that requires them.
INVALID_CREDENTIALS 401 The X-Client-Id/X-Client-Secret pair (or user email/password) is wrong.
TOKEN_EXPIRED 401 A refresh, password-reset, or email-verification token is invalid, expired, or already used.
ACCESS_DENIED 403 The credential is valid but not authorized for this resource or action.
QR_NOT_FOUND 404 No QR code matches the supplied identifier.
QR_INACTIVE 409 The QR code exists but has not been activated yet.
QR_EXPIRED 409 The QR code existed but is past its validity window.
QR_INVALID 422 The supplied QR payload is malformed or unrecognized.
QR_ALREADY_USED 409 A single-use QR code has already been redeemed.
SECURE_QR_REQUIRES_APP 403 This QR code requires the Qrauthix mobile app's secure-verification flow.
APP_NOT_REGISTERED 404 The mobile application id on the request is not registered.
DEVICE_NOT_REGISTERED 404 The calling device has not completed mobile device registration.
ATTESTATION_FAILED 403 Device/app attestation did not pass validation.
CHALLENGE_EXPIRED 410 A secure-verification challenge nonce expired before it was used.
CHALLENGE_INVALID 422 A secure-verification challenge nonce is malformed or already consumed.
RATE_LIMITED 429 Too many requests for this credential/IP in the current window — see Rate limits below.
VALIDATION_ERROR 422 Request body/query failed field-level validation; see the message for details.
NOT_FOUND 404 The requested resource (application, credential, webhook, organization, ...) does not exist.
CONFLICT 409 The request conflicts with the resource's current state.
DUPLICATE_EMAIL 409 Registration was attempted with an email address that is already in use.
REPLAY_DETECTED 409 A request/nonce that was already processed was submitted again.
INTERNAL_ERROR 500 An unexpected server error occurred; safe to retry with backoff.

Rate limits

The Developer API is rate-limited per client IP using a fixed window. The default developer policy allows 300 requests per 60 seconds. Exceeding it returns 429 Too Many Requests with error.code = "RATE_LIMITED". Back off and retry after the window elapses; there is no queueing.

Rate-limit responses are not guaranteed to include a request ID — check for the X-Request-ID response header instead when reporting an issue.

Request IDs

Every response includes an X-Request-ID header — the value you sent in that header on the request, or a generated one if you didn't send one. Log it on your side and include it when contacting support; it also appears as error.requestId on most error responses.