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

# Verify claims on your backend

> Never trust the phone. Check every claim again on your server, in TypeScript, Go or Rust.

`claim()` verifies the claim on the phone, but your backend must not trust the phone. Send `claim.encoded` (the canonical bytes, base64) to your backend and verify it there.

## Configure once

* **The operator public key**, PEM. Pin it in configuration; never fetch it.
* **The trust bundle URL.** Fetch it at least every 60 seconds and keep the highest version you accepted. Refuse a lower one. If you have no fresh copy for 30 minutes, stop accepting claims.

## The checks

1. **Signature**: Ed25519 over the canonical encoding, by a claim key the bundle lists for the claim's epoch, on a verifier that is not revoked, in a valid epoch.
2. **Time**: `issued_at` at most 60 seconds ahead, `expires_at` not passed.
3. **Holder binding**: refused unless your backend checks one.
4. **Policy**: `profile_id` and `policy_id` are the ones you asked for, and the outputs say what you need.
5. **Nullifier**: if an account may prove a fact once, refuse a nullifier you have seen.

The libraries do 1 to 3. Checks 4 and 5 are yours. Verify a claim when it arrives: a claim stops verifying once its epoch closes, which can be before `expires_at`.

<CodeGroup>
  ```ts TypeScript theme={null}
  // npm install @zktls/claim   (Node 22+)
  import { canonicalDecode, loadTrustBundle, verify, type TrustBundle } from "@zktls/claim";

  const OPERATOR_KEY = process.env.ZKTLS_TRUST_OPERATOR_KEY!;
  let highest = 0n;

  async function trustBundle(): Promise<TrustBundle> {
    const json = await (await fetch(process.env.ZKTLS_TRUST_BUNDLE_URL!)).text();
    const bundle = loadTrustBundle(json, OPERATOR_KEY, { minVersion: highest });
    highest = bundle.version;
    return bundle;
  }

  export async function accept(encoded: string, policyId: string) {
    const claim = canonicalDecode(Buffer.from(encoded, "base64"));
    const outputs = await verify(claim, await trustBundle(), BigInt(Math.floor(Date.now() / 1000)));
    if (claim.policy_id !== policyId) throw new Error(`claim is for ${claim.policy_id}`);
    return { outputs, nullifier: claim.nullifier };   // check the nullifier against your store
  }
  ```

  ```go Go theme={null}
  // go get github.com/Cr3dentials/zktls-consumers/consumers/go/zktlsclaim   (Go 1.27+)
  func startBundles(ctx context.Context, pinnedOperatorPEM []byte, url string) (*zktlsclaim.BundleClient, error) {
  	key, err := zktlsclaim.ParseOperatorKey(pinnedOperatorPEM)
  	if err != nil {
  		return nil, err
  	}
  	bundles := zktlsclaim.NewBundleClient(url, key, nil, nil)
  	go bundles.Run(ctx) // every 60 s, highest version only, stale after 30 min
  	return bundles, nil
  }

  func accept(bundles *zktlsclaim.BundleClient, encoded, policyID string) ([]zktlsclaim.TypedOutput, error) {
  	raw, err := base64.StdEncoding.DecodeString(encoded)
  	if err != nil {
  		return nil, err
  	}
  	c, err := zktlsclaim.ClaimCanonicalDecode(raw)
  	if err != nil {
  		return nil, err
  	}
  	b, err := bundles.Current()
  	if err != nil {
  		return nil, err
  	}
  	outputs, err := zktlsclaim.VerifyClaim(c, b, uint64(time.Now().Unix()))
  	if err != nil {
  		return nil, err
  	}
  	if c.PolicyID != policyID {
  		return nil, fmt.Errorf("claim is for %s", c.PolicyID)
  	}
  	return outputs, nil // then zktlsclaim.ConsumeNullifier(c, yourStore)
  }
  ```

  ```rust Rust theme={null}
  // cargo add zktls-claim
  use zktls_claim::bundle::{load_trust_bundle, OperatorKey};

  let key = OperatorKey::from_pem_or_der(PINNED_OPERATOR_PEM.as_bytes())?;
  let bundle = load_trust_bundle(&fetched_bundle_json, &key)?;
  if bundle.version < highest_seen {
      return Err("a stale trust bundle".into());
  }
  highest_seen = bundle.version;
  let claim = zktls_claim::canonical_decode(&base64_decoded_claim)?;
  let outputs = zktls_claim::verify(&claim, &bundle, now_unix_seconds)?;
  if claim.policy_id != expected_policy_id {
      return Err("not the policy asked for".into());
  }
  ```
</CodeGroup>

## Holder-bound claims

A policy may bind its claim to a key the user holds. `verify` refuses such a claim. When your backend has checked that the presenter controls that key (a challenge they signed), call `verifyWithHolder` (TypeScript), `VerifyClaimWithHolder` (Go) or `verify_with_holder` (Rust).

## On-chain

On Stellar, the [claims contract](/protocol/claims-contract) does checks 1 and 2 against registered verifier keys, and the orchestrator does 4 and 5 against the order.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.