Free of charge, no account, verifiable offline: your users prove that they have a Qevik account, and you check the signature on your server.
Do you sell items online? See the merchant API
Declare the domain
You declare your domain and publish a verification file. No account is needed.
The user proves their account
From your site, the user authorises on qevik.com the sending of a signed confirmation.
You check the signature
Your server checks the confirmation with Qevik's public keys, with no call at each display.
Choose a wording, a language and a background. The files are used as they are, with no translation or editing (terms, section 5).
Preview, as it appears on a listing
The badge goes next to the pseudonym (terms, section 5 b).
HTML snippet
<a href="https://qevik.com/verify/en#ATTESTATION" target="_blank" rel="noopener noreferrer" referrerpolicy="no-referrer">
<img src="/badges/qevik-badge-membre-en-clair.svg" alt="Qevik member" width="95" height="28"></a>The Platform displays a Qevik badge only:
The Platform does not combine the badge with any statement suggesting that Qevik verifies the user's identity, vouches for the user or their services, insures items, recommends the Platform or is its partner.
Your platform opens /authorize on qevik.com.
The user signs in to Qevik and reads the consent screen.
If they authorise, qevik.com sends them back to your return address with code, state and iss.
Your server checks state and iss, then exchanges the code at /token, server to server, along with the user's public pseudonym (platform_handle).
You receive the signed confirmation and check it on your server.
You ask the user whether they want to show the badge on their listings.
The choice belongs to the user
The terms require it (section 5 a): the badge only appears if the user has expressly chosen it, and disappears as soon as they withdraw that choice.
Before expiry, you renew the confirmation, sending the user's public pseudonym again; if the user has withdrawn access, the renewal is refused.
A Qevik QR code can only be downloaded on qevik.com. Your “Download” button opens the qevik.com page in a new tab or window, with rel="noopener noreferrer". Your platform never receives the QR code, cannot obtain it through the API and does not embed that page in a frame (iframe).
GET /api/v1/public/oauth/authorize
GET https://qevik.com/api/v1/public/oauth/authorize
?client_id=plt_YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fyour-site.example%2Fqevik%2Fback # exact registered return URL
&response_type=code
&scope=attest%3Aaccount%20attest%3Atagged # or attest:account alone
&state=RANDOM_BOUND_TO_THE_USER_SESSION # mandatory, <= 512 chars
&code_challenge=BASE64URL_SHA256_OF_CODE_VERIFIER
&code_challenge_method=S256 # S256 only
&ui_locales=fr # optional: fr, de or en
# No platform_handle here: it goes to POST /token only.
# Refusals are never redirects: the user sees a Qevik page.redirect_uri ?state&iss&code
# Approved
https://your-site.example/qevik/back?state=...&iss=https://qevik.com&code=... # code: single use, 60 s
# Refused by the user
https://your-site.example/qevik/back?state=...&iss=https://qevik.com&error=access_denied
# Check state against the user's session and iss against https://qevik.com before anything else.POST /api/v1/public/oauth/token (authorization_code)
POST https://qevik.com/api/v1/public/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=CODE_FROM_THE_CALLBACK
&code_verifier=ORIGINAL_CODE_VERIFIER
&client_id=plt_YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fyour-site.example%2Fqevik%2Fback
&platform_handle=jean_d # the user's public name on your site, on every call
# Server to server only: browsers are refused by CORS. No client secret (public client, PKCE).POST /api/v1/public/oauth/token (refresh_token)
POST https://qevik.com/api/v1/public/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=LAST_REFRESH_TOKEN_RECEIVED
&client_id=plt_YOUR_CLIENT_ID
&platform_handle=jean_d # sent again: Qevik never stores it
# Refresh tokens rotate: keep the new one, the one you sent is dead.
# Sending a used refresh token (or a code twice) revokes the link: the user must consent again.200 /token
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{
"attestation": "eyJhbGciOiJFUzI1NiIsImtpZCI6Ii4uLiIsInR5cCI6InFldmlrLWF0dGVzdGF0aW9uK2p3dCJ9...",
"token_type": "qevik-attestation",
"expires_in": 2592000,
"refresh_token": "...",
"scope": "attest:account attest:tagged"
}OAuth errors /token
HTTP/1.1 400 Bad Request
{ "error": "invalid_request", "error_description": "platform_handle is invalid (PLT_014)." }
400 invalid_request missing or malformed parameter, invalid platform_handle
400 invalid_grant code or refresh token unknown, expired, used or revoked; PKCE mismatch
400 access_denied platform closed, or the user can no longer be attested (link revoked)
400 unsupported_grant_type anything but authorization_code or refresh_token
400 temporarily_unavailable the offer is closed
429 temporarily_unavailable 5 000 attestations per platform per UTC day; honour Retry-After
# platform_handle: NFC, 1 to 64 characters, letters and digits of any script, . _ - and single inner spaces.Attestation claims
header { "alg": "ES256", "kid": "...", "typ": "qevik-attestation+jwt" }
payload {
"iss": "https://qevik.com",
"aud": "plt_YOUR_CLIENT_ID", // a single string
"sub": "...", // pairwise: differs on every platform
"iat": 1761120000, "nbf": 1761120000, "exp": 1763712000, // 30 days
"jti": "...",
"att": "tagged", // "account" or "tagged"
"member": true, // always true
"tagged": true, // present only when att = "tagged"
"since": 2025, // account creation year
"dom": "your-site.example",
"platformHandle": "jean_d" // must equal the listing owner's name
}The confirmation is a JWT signed by Qevik. Check it with Qevik's public keys and a well-established library.
Library: jose, version 5.2 or later.
import { createRemoteJWKSet, jwtVerify } from "jose";
const ISSUER = "https://qevik.com";
const AUDIENCE = "plt_YOUR_CLIENT_ID";
// JWKS cached for 1 h; an unknown kid reloads it at most once every 5 minutes.
const JWKS = createRemoteJWKSet(new URL("https://qevik.com/.well-known/qevik-platform-jwks.json"), {
cacheMaxAge: 3_600_000,
cooldownDuration: 300_000,
});
/** Returns the claims, or null: a null hides the badge, it never shows an error. */
export async function verifyQevikAttestation(token, expectedHandle) {
if (typeof token !== "string" || token.length > 2048 || token.split(".").length !== 3) return null;
try {
const { payload } = await jwtVerify(token, JWKS, {
algorithms: ["ES256"],
issuer: ISSUER,
audience: AUDIENCE,
typ: "qevik-attestation+jwt",
clockTolerance: 60,
requiredClaims: ["exp", "nbf", "iat", "sub", "att", "member", "platformHandle"],
});
if (payload.member !== true) return null;
if (payload.att !== "account" && payload.att !== "tagged") return null;
if (payload.platformHandle !== expectedHandle) return null;
return payload; // "Qevik label" badge only if payload.tagged === true
} catch {
return null;
}
}
Check on your server, never in the user's browser.
Before going live, each case must give the expected result with your implementation.
These vectors are signed with a test key published for this purpose only. Never accept it in production.
| Case | Expected result |
|---|---|
Valid confirmation valid | Accept |
Expired confirmation expired | Reject |
Tampered signature badSignature | Reject |
Audience of another platform (aud) wrongAud | Reject |
Algorithm “none” algNone | Reject |
Algorithm HS256 signed with the public key hs256 | Reject |
Unknown key (kid) unknownKid | Reject |
Pseudonym different from the listing's badHandle | Reject |
{
"issuer": "https://test-vectors.qevik.invalid",
"audience": "plt_TESTVECTORS0001",
"verifyAt": 1792670400,
"listingHandle": "jean_d",
"jwks": {
"keys": [
{
"kty": "EC",
"crv": "P-256",
"x": "u_zfhmLEuPswg6JpT7grZpo4m_NkCFxmicl5x4FsUvY",
"y": "mzG_VCMoJIHUojBIijnhn6JmEUKPoXN05gmL31A3nUY",
"kid": "qevik-test-vectors-2026-10-22",
"use": "sig",
"alg": "ES256"
}
]
},
"vectors": [
{
"case": "valid",
"expected": "accept",
"token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InFldmlrLXRlc3QtdmVjdG9ycy0yMDI2LTEwLTIyIiwidHlwIjoicWV2aWstYXR0ZXN0YXRpb24rand0In0.eyJpc3MiOiJodHRwczovL3Rlc3QtdmVjdG9ycy5xZXZpay5pbnZhbGlkIiwiYXVkIjoicGx0X1RFU1RWRUNUT1JTMDAwMSIsInN1YiI6InRlc3QtdmVjdG9ycy1wYWlyd2lzZS1zdWIiLCJpYXQiOjE3OTI2NjY4MDAsIm5iZiI6MTc5MjY2NjgwMCwiZXhwIjoxNzkzMjcxNjAwLCJqdGkiOiJ0ZXN0LXZlY3RvcnMtanRpIiwiYXR0IjoiYWNjb3VudCIsIm1lbWJlciI6dHJ1ZSwic2luY2UiOjIwMjUsImRvbSI6ImV4YW1wbGUuY29tIiwicGxhdGZvcm1IYW5kbGUiOiJqZWFuX2QifQ.LWxRJPwTJUIZZZ1upll0EBR4UNubVjLtMgP8aNZQ2DiatLLz3tNqPc6RKvSi1cm14NVFYZukls7WCgy0nahOVQ"
},
{
"case": "expired",
"expected": "reject",
"token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InFldmlrLXRlc3QtdmVjdG9ycy0yMDI2LTEwLTIyIiwidHlwIjoicWV2aWstYXR0ZXN0YXRpb24rand0In0.eyJpc3MiOiJodHRwczovL3Rlc3QtdmVjdG9ycy5xZXZpay5pbnZhbGlkIiwiYXVkIjoicGx0X1RFU1RWRUNUT1JTMDAwMSIsInN1YiI6InRlc3QtdmVjdG9ycy1wYWlyd2lzZS1zdWIiLCJpYXQiOjE3OTE4MDY0MDAsIm5iZiI6MTc5MTgwNjQwMCwiZXhwIjoxNzkyNTg0MDAwLCJqdGkiOiJ0ZXN0LXZlY3RvcnMtanRpIiwiYXR0IjoiYWNjb3VudCIsIm1lbWJlciI6dHJ1ZSwic2luY2UiOjIwMjUsImRvbSI6ImV4YW1wbGUuY29tIiwicGxhdGZvcm1IYW5kbGUiOiJqZWFuX2QifQ.m73cnY6lVdJBaZmsfvjCKIVmQhD4wSIKFBaD8iyD9FRrpVxNiNiPfHTTQvEq095J0QKAAF5kb_kOIZLQDNwlVA"
},
{
"case": "badSignature",
"expected": "reject",
"token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InFldmlrLXRlc3QtdmVjdG9ycy0yMDI2LTEwLTIyIiwidHlwIjoicWV2aWstYXR0ZXN0YXRpb24rand0In0.eyJpc3MiOiJodHRwczovL3Rlc3QtdmVjdG9ycy5xZXZpay5pbnZhbGlkIiwiYXVkIjoicGx0X1RFU1RWRUNUT1JTMDAwMSIsInN1YiI6InRlc3QtdmVjdG9ycy1wYWlyd2lzZS1zdWIiLCJpYXQiOjE3OTI2NjY4MDAsIm5iZiI6MTc5MjY2NjgwMCwiZXhwIjoxNzkzMjcxNjAwLCJqdGkiOiJ0ZXN0LXZlY3RvcnMtanRpIiwiYXR0IjoiYWNjb3VudCIsIm1lbWJlciI6dHJ1ZSwic2luY2UiOjIwMjUsImRvbSI6ImV4YW1wbGUuY29tIiwicGxhdGZvcm1IYW5kbGUiOiJqZWFuX2QifQ.AWxRJPwTJUIZZZ1upll0EBR4UNubVjLtMgP8aNZQ2DiatLLz3tNqPc6RKvSi1cm14NVFYZukls7WCgy0nahOVQ"
},
{
"case": "wrongAud",
"expected": "reject",
"token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InFldmlrLXRlc3QtdmVjdG9ycy0yMDI2LTEwLTIyIiwidHlwIjoicWV2aWstYXR0ZXN0YXRpb24rand0In0.eyJpc3MiOiJodHRwczovL3Rlc3QtdmVjdG9ycy5xZXZpay5pbnZhbGlkIiwiYXVkIjoicGx0X1RFU1RWRUNUT1JTQ0xPU0VEIiwic3ViIjoidGVzdC12ZWN0b3JzLXBhaXJ3aXNlLXN1YiIsImlhdCI6MTc5MjY2NjgwMCwibmJmIjoxNzkyNjY2ODAwLCJleHAiOjE3OTMyNzE2MDAsImp0aSI6InRlc3QtdmVjdG9ycy1qdGkiLCJhdHQiOiJhY2NvdW50IiwibWVtYmVyIjp0cnVlLCJzaW5jZSI6MjAyNSwiZG9tIjoiZXhhbXBsZS5jb20iLCJwbGF0Zm9ybUhhbmRsZSI6ImplYW5fZCJ9.Ci2dObZufGWY3v-DyWWrrYFaDrqdX_C1elzyrkOm-NslEY5zHWyXhFF9-W-T8n9PO55_eJD0BeRzAgg_9qHHiQ"
},
{
"case": "algNone",
"expected": "reject",
"token": "eyJhbGciOiJub25lIiwia2lkIjoicWV2aWstdGVzdC12ZWN0b3JzLTIwMjYtMTAtMjIiLCJ0eXAiOiJxZXZpay1hdHRlc3RhdGlvbitqd3QifQ.eyJpc3MiOiJodHRwczovL3Rlc3QtdmVjdG9ycy5xZXZpay5pbnZhbGlkIiwiYXVkIjoicGx0X1RFU1RWRUNUT1JTMDAwMSIsInN1YiI6InRlc3QtdmVjdG9ycy1wYWlyd2lzZS1zdWIiLCJpYXQiOjE3OTI2NjY4MDAsIm5iZiI6MTc5MjY2NjgwMCwiZXhwIjoxNzkzMjcxNjAwLCJqdGkiOiJ0ZXN0LXZlY3RvcnMtanRpIiwiYXR0IjoiYWNjb3VudCIsIm1lbWJlciI6dHJ1ZSwic2luY2UiOjIwMjUsImRvbSI6ImV4YW1wbGUuY29tIiwicGxhdGZvcm1IYW5kbGUiOiJqZWFuX2QifQ."
},
{
"case": "hs256",
"expected": "reject",
"token": "eyJhbGciOiJIUzI1NiIsImtpZCI6InFldmlrLXRlc3QtdmVjdG9ycy0yMDI2LTEwLTIyIiwidHlwIjoicWV2aWstYXR0ZXN0YXRpb24rand0In0.eyJpc3MiOiJodHRwczovL3Rlc3QtdmVjdG9ycy5xZXZpay5pbnZhbGlkIiwiYXVkIjoicGx0X1RFU1RWRUNUT1JTMDAwMSIsInN1YiI6InRlc3QtdmVjdG9ycy1wYWlyd2lzZS1zdWIiLCJpYXQiOjE3OTI2NjY4MDAsIm5iZiI6MTc5MjY2NjgwMCwiZXhwIjoxNzkzMjcxNjAwLCJqdGkiOiJ0ZXN0LXZlY3RvcnMtanRpIiwiYXR0IjoiYWNjb3VudCIsIm1lbWJlciI6dHJ1ZSwic2luY2UiOjIwMjUsImRvbSI6ImV4YW1wbGUuY29tIiwicGxhdGZvcm1IYW5kbGUiOiJqZWFuX2QifQ.dfPx7WKEdyHAkjyATNTFF3M1D1URSC56PfrrnHFYw8c"
},
{
"case": "unknownKid",
"expected": "reject",
"token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InFldmlrLXRlc3QtdmVjdG9ycy11bmtub3duIiwidHlwIjoicWV2aWstYXR0ZXN0YXRpb24rand0In0.eyJpc3MiOiJodHRwczovL3Rlc3QtdmVjdG9ycy5xZXZpay5pbnZhbGlkIiwiYXVkIjoicGx0X1RFU1RWRUNUT1JTMDAwMSIsInN1YiI6InRlc3QtdmVjdG9ycy1wYWlyd2lzZS1zdWIiLCJpYXQiOjE3OTI2NjY4MDAsIm5iZiI6MTc5MjY2NjgwMCwiZXhwIjoxNzkzMjcxNjAwLCJqdGkiOiJ0ZXN0LXZlY3RvcnMtanRpIiwiYXR0IjoiYWNjb3VudCIsIm1lbWJlciI6dHJ1ZSwic2luY2UiOjIwMjUsImRvbSI6ImV4YW1wbGUuY29tIiwicGxhdGZvcm1IYW5kbGUiOiJqZWFuX2QifQ.7BmSiWeOXySvC4AkJfRaRNXyOURPsgmoK1IHKEyzUydmg2cHK5o0JH9gAd01SaYBecdMKbzJ4Hhy4Vpnt4w69g"
},
{
"case": "badHandle",
"expected": "reject",
"token": "eyJhbGciOiJFUzI1NiIsImtpZCI6InFldmlrLXRlc3QtdmVjdG9ycy0yMDI2LTEwLTIyIiwidHlwIjoicWV2aWstYXR0ZXN0YXRpb24rand0In0.eyJpc3MiOiJodHRwczovL3Rlc3QtdmVjdG9ycy5xZXZpay5pbnZhbGlkIiwiYXVkIjoicGx0X1RFU1RWRUNUT1JTMDAwMSIsInN1YiI6InRlc3QtdmVjdG9ycy1wYWlyd2lzZS1zdWIiLCJpYXQiOjE3OTI2NjY4MDAsIm5iZiI6MTc5MjY2NjgwMCwiZXhwIjoxNzkzMjcxNjAwLCJqdGkiOiJ0ZXN0LXZlY3RvcnMtanRpIiwiYXR0IjoiYWNjb3VudCIsIm1lbWJlciI6dHJ1ZSwic2luY2UiOjIwMjUsImRvbSI6ImV4YW1wbGUuY29tIiwicGxhdGZvcm1IYW5kbGUiOiJtYXJpZV9rIn0.qVAM8tm0AMoL_npDpHe5JmTXOFzutksNPWwbDpbm0ySHCIQ0EUrLD_otABxiNTRaDFndF7XjhPaQ24NLO5U_eg"
}
]
}What your visitors will read on Qevik's verification page:
What this badge does not prove
Qevik does not verify anyone's identity. This badge does not vouch for its holder's honesty, listings, or the items or parcels they carry, and Qevik provides no insurance. It does not promise that a lost item will be found. Qevik is not a party to any dealings on the site showing the badge.
API terms of use (version 1.0)(new tab)
The API is free of charge and provided without any service level. No individual support is promised.