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
| Code | Typical status | Meaning |
|---|---|---|
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.