> ## Documentation Index
> Fetch the complete documentation index at: https://learn.biq.li/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify attribution receipts

> Verify Ed25519-signed mobile attribution receipts with Biqli's public JWKS.

<Warning>
  Mobile attribution receipts are part of the **Public Beta**. Verify them only on trusted backend infrastructure.
</Warning>

A matched live mobile-open response can contain `attributionReceipt`, a compact JSON Web Token (JWT) signed with Ed25519. Use it as short-lived evidence when a referral can grant money, credit, access, or another valuable benefit.

Never trust only the referral code, click ID, or decoded JWT body. A client can modify those values unless your backend verifies the signature and claims.

## Retrieve the JSON Web Key Set

```http theme={null}
GET https://biq.li/.well-known/biqli-attribution-jwks.json
```

Example:

```json theme={null}
{
  "keys": [
    {
      "kty": "OKP",
      "use": "sig",
      "alg": "EdDSA",
      "kid": "mobile-example",
      "crv": "Ed25519",
      "x": "base64url_encoded_public_key"
    }
  ]
}
```

This JSON Web Key Set (JWKS) endpoint returns an `ETag` and `Cache-Control: public, max-age=3600`. Cache the set for up to one hour, use conditional requests with `If-None-Match`, and refresh immediately once if an otherwise valid receipt uses an unknown `kid`. Do not pin a single key forever; key rotation can expose multiple verification keys.

The endpoint returns `404` when receipt verification is not available.

## Header contract

Require every header value:

| Header | Required value               |
| :----- | :--------------------------- |
| `alg`  | `EdDSA`                      |
| `typ`  | `JWT`                        |
| `kid`  | A current key ID in the JWKS |

Select only a JWK with `kty: OKP`, `use: sig`, `alg: EdDSA`, and `crv: Ed25519`. Decode its `x` member as the 32-byte Ed25519 public key and verify the compact JWT signature over the original encoded header and payload segments.

Do not accept an algorithm from application input, fall back to another algorithm, or treat a decoded payload as verified.

## Claim contract

| Claim         | Meaning and required check                                                                                   |
| :------------ | :----------------------------------------------------------------------------------------------------------- |
| `iss`         | Require exactly `https://biq.li`.                                                                            |
| `aud`         | Require the exact `biq_mapp_...` Mobile App ID expected by your backend.                                     |
| `jti`         | Unique receipt ID. Store and consume it idempotently.                                                        |
| `iat`         | Issued-at Unix timestamp. Reject unreasonable future values.                                                 |
| `nbf`         | Not-before time. Biqli allows a five-second signing skew.                                                    |
| `exp`         | Expiration time. Receipts last at most ten minutes.                                                          |
| `workspaceId` | Public workspace ID that owns the app. Compare it when your backend is workspace-aware.                      |
| `appId`       | Public Mobile App ID. It must equal the expected app and audience.                                           |
| `openId`      | Mobile-open event ID. Bind it to the operation being processed.                                              |
| `clickId`     | Matched click ID. Compare it with the submitted attribution context.                                         |
| `linkId`      | Matched link ID. Apply any offer or campaign allowlist.                                                      |
| `matchType`   | `exact_app_link`, `exact_install_referrer`, `exact_handoff`, or `probabilistic`.                             |
| `confidence`  | `exact` or `probabilistic`. Require the confidence your operation allows.                                    |
| `attribution` | Signed referral code, safe metadata, and allowed dynamic values. Validate their business meaning separately. |

Use a small, documented clock-skew allowance when checking `nbf`, `iat`, and `exp`. Never extend `exp` locally.

## Idempotent reward flow

<Steps>
  <Step title="Receive the live receipt">
    Send it from the app to your authenticated backend immediately after the matched resolver response. Use TLS and do not place it in a URL.
  </Step>

  <Step title="Verify cryptography and claims">
    Verify the Ed25519 signature, fixed header values, issuer, audience, time claims, app, link, match type, and confidence before reading referral data.
  </Step>

  <Step title="Apply your own eligibility rules">
    Confirm the signed referral is valid for the authenticated user, campaign, product, geography, and operation. A valid receipt proves Biqli attribution, not your business eligibility.
  </Step>

  <Step title="Consume jti atomically">
    In the same transaction that grants the reward, insert `jti` into a unique consumed-receipt store. If it already exists, return the original outcome instead of granting again.
  </Step>
</Steps>

Success means the reward and receipt ID commit once. A network retry with the same receipt returns the previously recorded result.

## PHP verification outline

Use a maintained JOSE library that supports EdDSA and OKP JWKs when possible. If your implementation uses Sodium directly, the critical signature operation is equivalent to:

```php theme={null}
<?php

[$encodedHeader, $encodedPayload, $encodedSignature] = explode('.', $receipt, 3);
$signingInput = $encodedHeader.'.'.$encodedPayload;

$decode = static function (string $value): string {
    $value = strtr($value, '-_', '+/');
    $value .= str_repeat('=', (4 - strlen($value) % 4) % 4);
    $decoded = base64_decode($value, true);
    if ($decoded === false) {
        throw new RuntimeException('Invalid base64url value.');
    }
    return $decoded;
};

$header = json_decode($decode($encodedHeader), true, 16, JSON_THROW_ON_ERROR);
$claims = json_decode($decode($encodedPayload), true, 32, JSON_THROW_ON_ERROR);

if (($header['alg'] ?? null) !== 'EdDSA' || ($header['typ'] ?? null) !== 'JWT') {
    throw new RuntimeException('Unexpected receipt header.');
}

// Select the matching kid from the cached JWKS before decoding x.
$publicKey = $decode($matchingJwk['x']);
$signature = $decode($encodedSignature);

if (!sodium_crypto_sign_verify_detached($signature, $signingInput, $publicKey)) {
    throw new RuntimeException('Invalid receipt signature.');
}

// Next validate iss, aud, nbf, iat, exp, appId, IDs, confidence, and unique jti.
```

This outline deliberately does not provide storage or authorization logic. Your backend must implement atomic replay prevention and business eligibility.

## Failure handling

Reject the receipt when:

* it has other than three compact segments;
* decoding, JSON parsing, or Ed25519 verification fails;
* the algorithm, type, key type, curve, use, or key ID is unexpected;
* issuer, audience, app, workspace, link, click, or open does not match the operation;
* it is early, expired, or unreasonably future-dated;
* `jti` was already consumed for a different operation; or
* your policy does not accept the match type or confidence.

A missing receipt is normal for `none`, for a cached SDK result, or when receipts are unavailable. It is not evidence that should bypass verification.
