Badge Check-in Integration
Badge Check-in verifies that the person checking in is your enrolled rep — and that their badge is genuine — from a single selfie. Your backend makes one authenticated call and gets back a clear pass/fail with a confidence score.
Before you start
You’ll need:
- Credentials: a client ID + secret and the token endpoint, issued by Parselo.
- The scope
checkin/verify. - A reference-photo endpoint on your backend that Parselo calls to fetch each rep’s
enrollment photo — see Reference Photo Endpoint below for the
exact contract to implement. Set its base URL on your dashboard’s Badge Check-in page (or
send it with each request — see Per-request overrides), then
click Generate token — Parselo creates the credential for you; copy it into your own
backend’s config so it can check the
Authorizationheader on incoming requests.
1. Get an access token
Badge Check-in authenticates machine-to-machine, via OAuth2 client_credentials — this is a
service credential held by your backend, not a per-rep login.
curl -X POST https://<YOUR_AUTH_DOMAIN>/oauth2/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "<CLIENT_ID>:<CLIENT_SECRET>" \
-d "grant_type=client_credentials&scope=checkin/verify"
Response:
{ "access_token": "<JWT>", "expires_in": 3600, "token_type": "Bearer" }
2. Verify a check-in
POST /verify-checkin with the rep id, the mode, and the selfie + badge as base64.
curl -X POST https://<PARSELO_API_HOST>/verify-checkin \
-H "Authorization: Bearer <ACCESS_TOKEN>" \
-H "Content-Type: application/json" \
-d '{
"repId": "<REP_ID>",
"mode": "3-leg",
"selfie": "<BASE64_JPEG>",
"badge": "<BASE64_JPEG>"
}'
3. Read the result
{
"decision": "yes",
"mode": "3-leg",
"confidence": 96,
"faceMatch": {
"selfieVsProfile": { "score": 96, "threshold": 90, "passed": true, "evaluated": true },
"selfieVsBadge": { "score": 88, "threshold": 80, "passed": true, "evaluated": true },
"badgeVsProfile": { "score": 91, "threshold": 80, "passed": true, "evaluated": true }
},
"badge": {
"valid": true,
"confidence": 82,
"checks": {
"qrPresent": true, "qrResolved": true, "repIdMatch": true,
"registrationNumberPresent": true,
"requiredTexts": [{ "text": "ACME Badge", "found": true }],
"charityLogoDetected": true,
"partnerLogoDetected": true,
"facePresent": true
}
},
"reasons": [],
"requestId": "…",
"processedRegion": "ca-central-1"
}
The two fields most callers act on: decision ("yes", "no", or "review") and
confidence (0–100, the primary face match). faceMatch and badge carry the per-leg and
badge-template detail for callers who want to show more than a pass/fail. reasons lists
machine-readable codes explaining a "no" or "review" decision (e.g.
"required_text_not_found:ACME Badge") — empty on a clean "yes". A "yes" can still list
a required_text_not_found reason when that miss was within the Required texts allowed to
be missing setting on your dashboard (at least one required text must always be found).
Note: requiredTexts is always an array of { "text": string, "found": boolean } objects —
one per required text checked (your account’s, or the ones you sent with the request) — never
a plain array of strings.
Go example
Steps 1 and 2 combined, using only the standard library — adapt to your own HTTP client.
// 1. Get a token (client_credentials).
form := url.Values{"grant_type": {"client_credentials"}, "scope": {"checkin/verify"}}
tokReq, _ := http.NewRequest("POST", authDomain+"/oauth2/token",
strings.NewReader(form.Encode()))
tokReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
tokReq.SetBasicAuth(clientID, clientSecret)
// ... do request, decode {access_token} into `token` ...
// 2. Verify a check-in.
body, _ := json.Marshal(map[string]string{
"repId": repID, "mode": "3-leg", "selfie": selfieB64, "badge": badgeB64,
})
req, _ := http.NewRequest("POST", parseloHost+"/verify-checkin", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
// ... do request, decode the response, branch on result.Decision ...
Modes
mode is 2-leg (selfie compared against the enrolled profile photo) or 3-leg (adds two
cross-checks against the badge’s own photo). Your account has a default mode; pass mode
explicitly to override it per request.
Per-request overrides
Like mode, the required badge text(s) and the reference-photo base URL set on your
dashboard are defaults. Send either one with a /verify-checkin call to override it for that
call only — useful when one account serves several campaigns, each with its own badge wording
or photo endpoint. Omit a field (don’t send null) to use your account’s setting.
| Field | Type | Overrides |
|---|---|---|
requiredTexts | array of 1–10 strings, each up to 100 characters | The text(s) that must appear on the badge. Replaces your account’s list for this call — it doesn’t add to it. |
referencePhotoBaseUrl | https:// URL | The base URL of your Reference Photo Endpoint. |
{
"repId": "<REP_ID>",
"mode": "3-leg",
"requiredTexts": ["ACME Badge"],
"referencePhotoBaseUrl": "https://api.yourcompany.com",
"selfie": "<BASE64_JPEG>",
"badge": "<BASE64_JPEG>"
}
- The reference-photo token can’t be overridden. You still generate it once on the dashboard, and Parselo sends it to whichever base URL applies — so only send base URLs of endpoints you control.
- The other badge checks (logos, registration number, QR) and the number of required texts allowed to be missing always follow your account’s settings. That allowance applies to the texts you send too, but at least one of them must always be found — so a single sent text is always strictly required.
- An invalid value is rejected with
400and one ofinvalid_requiredTexts,text_too_long,too_many_texts, orinvalid_referencePhotoBaseUrl— before anything is checked or billed.
Reference Photo Endpoint
This is the other half of the integration: an endpoint you host, that we call, to
fetch each rep’s enrollment photo by repId at verification time. We never store it —
every check-in fetches it fresh and discards it after comparison.
Where this fits. Your app calls our
POST /verify-checkin(above) with arepIdand a live selfie. We call your endpoint below with that samerepId, compare the bytes we get back against the selfie, and return the result to you. No translation of the id happens on either side — it’s the samerepIdthroughout.Configure it: on your dashboard’s Badge Check-in page, under Reference photo integration, set your endpoint’s base URL (or send
referencePhotoBaseUrlper request — see Per-request overrides), then click Generate token. Parselo creates the credential — you never invent one yourself — and shows it to you exactly once. Copy it into your own backend’s config (an env var, a secrets manager, whatever you already use) so your endpoint can check it on every incoming request. That’s the only setup step — nothing here requires a Parselo redeploy or a support ticket. To rotate later, click Rotate on the same card and update your backend’s copy; the old token stops working immediately.
Contract
| Method | GET |
| Path | {your base URL}/api/v1/reps/{repId}/photo |
| Auth | Authorization: Bearer <the token Parselo generated for you> |
| Success | 200 — raw image bytes |
| Not on file | 404 — a normal outcome, not an error |
curl -H "Authorization: Bearer <YOUR_TOKEN>" \
https://your-api.example.com/api/v1/reps/a1b2c3d4/photo \
-o reference.png
Success response — 200
Body is the raw image bytes — not JSON, not a signed URL.
Content-Type: image/png # or image/jpeg — whatever's actually on file
Cache-Control: no-store # recommended: a rep's photo can change between calls
Error responses
Every non-200 response should use one JSON envelope:
{
"error": {
"code": "not_found",
"message": "reference photo not found",
"request_id": "a1c9e2f0...",
"timestamp": "2026-09-29T03:31:36Z"
}
}
| Status | Meaning | code |
|---|---|---|
400 | repId was empty/missing in the path — shouldn’t happen from us, but validate defensively | bad_request |
401 | Missing/malformed header, or a token that doesn’t match | authentication_required |
404 | No reference photo on file for this rep — a normal outcome. We treat it as no/review, no retry. | not_found |
500 | Unexpected failure on your side. We treat this as a real error, distinct from 404 — safe for you to fix and have us retry. | internal_error |
What’s not supported
- Bytes only — no signed-URL response mode.
- No transformation — we use exactly what you return; resize/crop/re-encode on your side if needed.
- One active token at a time — there’s no dual-key overlap window. Clicking Rotate on the dashboard issues a new one and invalidates the old one immediately.
- You can’t choose your own token — Parselo always generates it, the same way it generates your Badge Check-in client secret. This is deliberate: you never have to invent a secret and keep it in sync across two systems by hand.