How a web application verifies a carrier
The shape that works: the browser is a camera and a display, the server holds the key, AffixIO is the verifier. Nothing in that sentence is optional.
- Capture The browser opens the camera or accepts a pasted carrier. Decode it with any QR or barcode library. You now hold a string, not a decision.
- Authenticate on your origin A short-lived session cookie or bearer token that your own backend issued to the operator. This is what authorises the next hop, not an AffixIO key.
- Forward Your backend calls
POST /v1/verify or POST /v1/gate/verify with the AffixIO key and the carrier payload. The key never leaves that host.
- Decide The browser receives a boolean, a reason code and whatever of the response you choose to surface. On a 409 it receives a double-spend signal and stops, rather than retrying.
- Audit Your backend keeps
X-Request-Id, the digest and the reason code. Optionally it listens for the matching webhook or pulls the evidence export later.
// browser: never holds AFFIX_API_KEY
const carrier = await scan(); // string from the camera
const session = await getOperatorSession(); // your own cookie / token
const result = await fetch("/internal/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${session}`,
},
body: JSON.stringify({ carrier }),
});
const { admitted, reason_code, request_id } = await result.json();
renderGate(admitted, reason_code, request_id);
// your backend: the only place that holds AFFIX_API_KEY
app.post("/internal/verify", requireOperator, async (req, res) => {
const upstream = await fetch("https://api.affix-io.com/v1/gate/verify", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.AFFIX_API_KEY}`,
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ carrier: req.body.carrier, gate_id: process.env.GATE_ID }),
});
const body = await upstream.json();
res.status(upstream.status).json({
admitted: body.admitted ?? body.valid ?? false,
reason_code: body.reason_code,
request_id: upstream.headers.get("X-Request-Id"),
});
});
Two separate credentials, two separate lifetimes. The operator session lasts for a shift. The AffixIO key lasts until you rotate it. Conflating them is how keys end up in source maps.