Callback Verification¶
After the user completes a Yanez flow, YanezYID POSTs the signed result directly to the partner backend's callback URL — not through the client. The callback may arrive from a different device (cross-device QR scan).
The callback endpoint is not authenticated by the user's session. Its trust
comes entirely from the single-use challenge: the request_id must map to a
pending, unexpired challenge that the backend issued, and the signed bytes must
equal that challenge exactly.
Callback Payload¶
YanezYID POSTs application/json:
{
"request_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"yid": "<stable unique yid>",
"message": "<the exact base64url message from the deep link>",
"keys": [
{ "threshold_tier": "low", "group_public_key": "0x<48-byte G1 hex>", "eth_address": "0x<EIP-55>" },
{ "threshold_tier": "medium", "group_public_key": "0x<48-byte G1 hex>", "eth_address": "0x<EIP-55>" },
{ "threshold_tier": "high", "group_public_key": "0x<48-byte G1 hex>", "eth_address": "0x<EIP-55>" }
],
"matched_tier": "high",
"signature": "0x<96-byte G2 hex>"
}
| Field | Notes |
|---|---|
request_id |
Correlates the callback to the challenge the backend issued. |
yid |
Stable unique Yanez ID for the user. |
message |
The exact message value from the deep link, base64url-encoded. |
keys |
One entry per threshold tier. The app always sends all registered tiers (enrollment and step-up alike); for step-up only the matched_tier entry is strictly required to verify. |
matched_tier |
The tier whose key produced signature. |
signature |
A single BLS12-381 G2 signature (96 bytes), produced by the matched tier's key. |
Successful callbacks include both matched_tier and signature. In redirect
mode, parse them as optional query parameters: if either is absent, the callback
cannot pass Verification Steps 3–4.
The callback carries no verification-status flag. The backend MUST independently verify the BLS signature (see Verification Steps) and never trust any client-asserted "verified" state.
Delivery Modes¶
The deep link's method parameter selects how YanezYID returns the result:
method |
Delivery |
|---|---|
post |
The payload above is sent as an application/json POST body to the callback URL. |
redirect |
The same fields are returned as query parameters appended to the callback URL (keys is serialized as a JSON string parameter). |
There is no callback-domain allowlist on either platform: post delivers to
whatever URL the signed link carries. Use an HTTPS endpoint on your own backend
that is reachable from the public internet — the request comes from the user's
phone, not from Yanez servers.
Respond to a post callback with 2xx. YanezYID treats any 2xx as
delivered. Any other status is shown to the user as a failed signing — a
STATUS · TITLE header over the message — and is never retried, so the user
must start a new signing. The app maps statuses to messages as follows; return
the specific ones deliberately or not at all:
| Your response | Return it when | What the user sees |
|---|---|---|
2xx |
Every verification step passes. | Success — the app dismisses silently. |
400 |
The payload can't be parsed: missing fields, bad encoding, or an invalid matched_tier (step 3). |
400 · BAD REQUEST — "Partner rejected the request." |
401 |
The signature, keys, or message fail verification (steps 2, 4–7, or 9). | 401 · SIGNATURE REJECTED — "Partner rejected the signature." |
403 |
Step-up: matched_tier is below required_tier (step 8), or your policy denies the action. |
403 · STEP-UP REQUIRED — "Additional verification required or denied." |
404 |
request_id is unknown (step 1). |
404 · CHALLENGE NOT FOUND — "Unknown or expired signing request." |
409 |
request_id was already consumed — a duplicate or replayed delivery (step 1). |
409 · ALREADY USED — "This request was already signed or is a duplicate." |
410 |
request_id has expired (step 1). |
410 · CHALLENGE EXPIRED — "This signing request has expired." |
5xx |
Your backend failed before it could verify. | 5xx · PARTNER ERROR — "The partner service had an error." |
other 4xx |
Anything else. | 4xx · REJECTED — "Partner rejected the delivery." |
If the phone gets no HTTP response at all, the user sees CONNECTION FAILED —
"Couldn't reach the partner. Please try again." — which also isn't retried.
The verification steps below are identical for both modes — always verify the
signature over the decoded message bytes regardless of how the result arrived.
Verification Steps¶
All of the following must hold before the callback is accepted. Mark the challenge consumed on first receipt, regardless of outcome — challenges are single-use even if verification fails.
request_idmaps to a stored challenge that is not expired and not consumed. Mark it consumed immediately.- Decoded
messagebytes are byte-for-byte equal to the canonical challenge the backend issued for thisrequest_id. matched_tieris a valid tier and is present inkeys.- The matched key's
group_public_keyis 48 bytes;signatureis 96 bytes. - BLS verifies
signatureagainst the matchedgroup_public_keyover the decodedmessagebytes. - For each entry in
keys,eth_addressequalsderive_address(group_public_key). Store the server-derived address — do not trust the client-supplied value. - Enrollment only: all tiers in
keyspass step 6; store the full key set with deduplication. - Step-up only:
matched_tier ≥ required_tier, and the matchedgroup_public_keyequals the key stored for this user and tier at enrollment. - (Recommended) Call
POST /api/partners/records/validateto confirm the(yid, group_public_key)pair against the Yanez registry.
BLS Verification¶
The signature scheme is BLS12-381 G2Basic with DST
BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_NUL_.
from py_ecc.bls import G2Basic
PUBKEY_LEN, SIG_LEN = 48, 96
def verify_bls(pubkey_g1: bytes, message: bytes, signature_g2: bytes) -> bool:
if len(pubkey_g1) != PUBKEY_LEN or len(signature_g2) != SIG_LEN:
return False
try:
return bool(G2Basic.Verify(pubkey_g1, message, signature_g2))
except Exception:
return False
message is the decoded (raw bytes) value of the message field — the same
bytes YanezYID signed verbatim.
Address Derivation¶
from eth_utils import keccak, to_checksum_address
def derive_address(pubkey_g1: bytes) -> str:
# EIP-55 checksum address derived from the BLS G1 public key.
# This is a stable identifier — it is NOT a usable Ethereum address.
return to_checksum_address(keccak(pubkey_g1)[-20:])
Client Result Delivery¶
The client learns the outcome by polling a status endpoint or via push (SSE) —
never as a direct reply to the signing hop. Correlate by request_id.