Skip to content

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.

  1. request_id maps to a stored challenge that is not expired and not consumed. Mark it consumed immediately.
  2. Decoded message bytes are byte-for-byte equal to the canonical challenge the backend issued for this request_id.
  3. matched_tier is a valid tier and is present in keys.
  4. The matched key's group_public_key is 48 bytes; signature is 96 bytes.
  5. BLS verifies signature against the matched group_public_key over the decoded message bytes.
  6. For each entry in keys, eth_address equals derive_address(group_public_key). Store the server-derived address — do not trust the client-supplied value.
  7. Enrollment only: all tiers in keys pass step 6; store the full key set with deduplication.
  8. Step-up only: matched_tier ≥ required_tier, and the matched group_public_key equals the key stored for this user and tier at enrollment.
  9. (Recommended) Call POST /api/partners/records/validate to 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.