> For the complete documentation index, see [llms.txt](https://docs.t-rex.network/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.t-rex.network/build/integrate-as-a-claim-issuer.md).

# Integrate as a Claim Issuer

A Claim Issuer verifies participants off-chain and attests the result as claims on their ONCHAINID. This page is for a team that already runs verifications (identity, accreditation, residency) and wants them to count for assets on T-REX: what to obtain, what to sign, how to deliver it, and what to run around it. The operations themselves are in [05 Issue a claim to an investor](/guides/claim-issuer/05.-issue-a-claim-to-an-investor.md) and [19 Revoke a claim](/guides/claim-issuer/19.-revoke-a-claim.md).

| Element          | Where it lives                                                                  |
| ---------------- | ------------------------------------------------------------------------------- |
| The verification | Off-chain, in the Claim Issuer's own process. Nothing of it is on-chain.        |
| The Claim Issuer | An ONCHAINID of type `CLAIM_ISSUER`. Its address is the `issuer` of its claims. |
| A claim          | Stored on the Investor's ONCHAINID. It serves every address of the identity.    |
| Validity         | Answered by the issuer's identity each time it is asked. No verdict is stored.  |
| Trust            | Per asset: each Issuer lists the Claim Issuers it trusts, per topic.            |

Claims feed eligibility: who may hold. Compliance, the rules on movements, is a separate gate. One shipped rule reuses the eligibility check: `SpenderVerificationModule` requires the spender of a `transferFrom` to be eligible.

## Get the identity and its keys

A `CLAIM_ISSUER` identity cannot be self-deployed: `IdentityFactory.createIdentity` reverts with `IdentityTypeNotSelfDeployable`. A party that holds the creation role of the type, granted by Network governance, calls `IdentityFactory.createIdentityFor(account, identityType, salt, keys)` with type 5: see [02 Deploy an ONCHAINID](/guides/identity/02.-deploy-an-onchainid.md).

`account` is an address the Claim Issuer controls: an externally owned account or a smart-contract wallet. It is linked as the first wallet of the identity. Every key passed must be a key of `account` (`KeyNotForAccount`), and `account` ends up as the only `MANAGEMENT` key (`AccountNotSoleManagementKey`). The party that deployed the identity holds no key on it.

The Claim Issuer then adds its other keys: [22 Add and remove keys on an ONCHAINID](/guides/identity/22.-add-and-remove-keys-on-an-onchainid.md).

| Purpose            | A Claim Issuer uses it for                                                                  | Signer                                                                                                                       |
| ------------------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `MANAGEMENT` (1)   | `addClaimTo`, `revokeClaimByDigest`, adding and removing keys                               | For the first two, an address that calls the identity directly: an externally owned account or a smart-contract wallet       |
| `CLAIM_SIGNER` (3) | Signing claims                                                                              | Any supported signer: an externally owned account, an ERC-1271 contract, or a key behind an ERC-7913 verifier (passkey, RSA) |
| `ACTION` (2)       | Ordering `addClaim` and `removeClaim` on an Investor's identity from the issuer's ONCHAINID | An address that calls `execute` on the identity, or any supported signer in a user operation                                 |

What to build: dedicated `CLAIM_SIGNER` keys for the signing service, and no claim signed with a `MANAGEMENT` key. That key satisfies every purpose check, so removing `CLAIM_SIGNER` from it withdraws nothing. An `ACTION` key cannot sign a claim, but it can have the issuer's identity remove, and so revoke, any claim of the issuer: hold it as tightly as `MANAGEMENT`.

## The anatomy of a claim

| Field               | Content                                                                        | Rule                                          |
| ------------------- | ------------------------------------------------------------------------------ | --------------------------------------------- |
| `topic`             | The fact attested, as a number                                                 | Not zero                                      |
| `scheme`, `uri`     | How the claim is read, and where supporting material is                        | Bound to the signature through `metadataHash` |
| `issuer`            | The ONCHAINID of the Claim Issuer                                              |                                               |
| `signature`         | The signer and its signature, in one blob                                      |                                               |
| `data.issuedAt`     | The time of signing, in seconds                                                | Not zero                                      |
| `data.validUntil`   | The time after which the claim expires                                         | Zero for no expiry                            |
| `data.metadataHash` | The hash of `scheme` and `uri`                                                 | Must match the values submitted               |
| `data.payload`      | The content for the topic: a hash or an encoded value, never raw personal data |                                               |

`data` is the struct `Structs.ClaimData`: `issuedAt` and `validUntil` as `uint256`, `metadataHash` as `bytes32`, `payload` as `bytes`, in this order. The signature, the payload and the URI are limited to 2,048 bytes each.

A claim is stored under `claimId = keccak256(abi.encode(issuer, topic))`. An identity holds one claim per issuer and topic, and adding another replaces it. The model is described in [Claims: issuance, validity, revocation](/identity-onchainid/claims-issuance-validity-revocation.md).

## Sign a claim

The signing key signs the ERC-712 digest returned by `getClaimHash(identity, topic, data)`, read on the Claim Issuer's ONCHAINID, with `identity` the Investor's ONCHAINID.

* **Domain:** name `OnchainID`, version `1`, the chain id, and the Claim Issuer's ONCHAINID as verifying contract. Read it from the identity with `eip712Domain()` instead of hardcoding it.
* **Types:** `Claim(uint256 topic,address subject,ClaimData data)`, `ClaimData(uint256 issuedAt,uint256 validUntil,Metadata metadata,bytes payload)`, `Metadata(uint256 scheme,string uri)`.
* **`metadataHash`:** the hash of `Metadata`, read with `ERC734Validator.getMetadataHash(scheme, uri)`. The function is on the module, at the address `Identity.registryModule()` returns. The identity does not expose it.

The `signature` field is `abi.encode(bytes signer, bytes rawSignature)`. `keccak256(signer)` is the key hash that must hold `CLAIM_SIGNER`.

| Signing key              | `signer`                                  | `rawSignature`                                 |
| ------------------------ | ----------------------------------------- | ---------------------------------------------- |
| Externally owned account | The 20 bytes of the address               | `abi.encodePacked(r, s, v)`                    |
| Smart-contract wallet    | The 20 bytes of the address               | What its ERC-1271 check accepts for the digest |
| Passkey, RSA key         | The verifier address, then the public key | What the verifier accepts for the digest       |

The digest binds the claim to its subject, its issuer and its chain. It does not include the signer: the same data signed by another key has the same digest.

## Deliver a claim

| Way                                        | Call                                                                                        | Applies when                                                                                                                             |
| ------------------------------------------ | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| The Claim Issuer adds it, with standing    | `addClaimTo(topic, scheme, signature, data, uri, identity)` on the Claim Issuer's ONCHAINID | The Claim Issuer has standing: type `CLAIM_ISSUER` and a reputation at or above the claim-add threshold. The Investor does not transact. |
| The Claim Issuer adds it, with a key grant | The same call                                                                               | The Investor's identity gave the issuer's ONCHAINID a `CLAIM_SIGNER` or `CLAIM_ADDER` key. Standing is not consulted.                    |
| The Identity Owner adds it                 | `addClaim(topic, scheme, issuer, signature, data, uri)` on the Investor's ONCHAINID         | Any other case. The Claim Issuer hands the signed claim to the Investor, and a claim key of its identity submits it.                     |

`addClaimTo` checks, in this order:

1. The caller holds `MANAGEMENT` on the issuer's identity. Otherwise `SenderDoesNotHaveManagementKey`.
2. The claim is `Valid` for this issuer. Otherwise `InvalidClaim`.
3. The issuer's ONCHAINID holds a claim key on the Investor's identity. If not: `IdentityFactory.identityTypeOf(issuer)` is `CLAIM_ISSUER`, then `ReputationRegistry.reputationOf(issuer)` is at least `claimAddThreshold()`. Otherwise `IdentityNotClaimIssuerType` or `ReputationBelowClaimAddThreshold`.
4. The topic is not zero (`InvalidClaimTopic`), and the signature, the payload and the URI are within their limits (`ClaimSignatureTooLong`, `ClaimPayloadTooLong`, `ClaimUriTooLong`).
5. `data.metadataHash` matches the scheme and the URI. Otherwise `ClaimMetadataMismatch`.
6. `isClaimValid` on the issuer returns true. Otherwise `InvalidClaim`.

`addClaim` by the Identity Owner checks that the caller holds a claim key on the Investor's identity (`SenderDoesNotHaveClaimSignerKey`), then 4 to 6.

> **Direct calls only.** `addClaimTo`, `revokeClaimByDigest`, and the Identity Owner's `addClaim` and `removeClaim` are called on the identity by the key holder itself. A call relayed as a user operation, or dispatched by the identity to itself, carries no key holder and is refused.

The issuer's identity can also be the caller. A key that holds `ACTION` or `MANAGEMENT` on it calls `execute(to, value, data)` on the issuer's ONCHAINID, with the Investor's identity as `to` and the encoded `addClaim` or `removeClaim` call as `data`, or sends the same call in a user operation. Checks 3 to 6 apply. `execute` does not revert when the inner call fails: the queue emits `ExecutionFailed` in place of `Executed`, so a batch process reads the event and does not rely on a revert.

## How a claim is evaluated

At every eligibility check, `TREXRegistry.isVerified(address)` takes each required topic and each Claim Issuer trusted for it, reads the claim stored under that issuer's `claimId` on the identity, and calls `isClaimValid(identity, topic, signature, data)` on the issuer. One valid claim per required topic is enough.

The issuer's identity decides in this order. The first condition met gives the status.

| Condition                                                                   | Status         |
| --------------------------------------------------------------------------- | -------------- |
| `issuedAt` is zero                                                          | `BadSignature` |
| The block time is before `issuedAt`                                         | `NotYetValid`  |
| `validUntil` is not zero and the block time is after it                     | `Expired`      |
| The digest is marked revoked on the issuer                                  | `Revoked`      |
| The signature blob is shorter than 128 bytes, or its signer shorter than 20 | `BadSignature` |
| The signer does not hold `CLAIM_SIGNER` on the issuer's identity            | `NotIssued`    |
| The signature does not verify against the digest                            | `BadSignature` |
| None of the above                                                           | `Valid`        |

`isClaimValid` returns true for `Valid` only. `getClaimStatus(identity, topic, signature, data)` returns the status. It is a function of the `ERC734Validator` that takes the issuer from the caller, and the reference module set of an identity routes `isClaimValid`, not `getClaimStatus`. Read it with an `eth_call` to the module, with `from` set to the Claim Issuer's ONCHAINID.

When a key loses `CLAIM_SIGNER`, every claim it signed reads `NotIssued` from the next check. The status is computed at each call: giving the purpose back to the same key makes those claims `Valid` again. A revocation and an expiry are final.

## Renew and revoke

| Need                               | What to do                                                                                                                                                                                                                                                  |
| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| End on a known date                | Set `validUntil`. Expiry needs no transaction.                                                                                                                                                                                                              |
| Renew                              | Sign new `ClaimData`, with a new `issuedAt` and `validUntil`, and deliver it. It replaces the stored claim.                                                                                                                                                 |
| Withdraw one claim                 | `revokeClaimByDigest(digest)` on the Claim Issuer's ONCHAINID, by a `MANAGEMENT` key. It reverts with `ClaimAlreadyRevoked` for a digest already revoked, and does not check that a claim exists for the digest. `isDigestRevoked(digest)` reads the state. |
| Delete the claim from the identity | `removeClaim(claimId)` on the Investor's identity, called by the issuer's ONCHAINID. Accepted for its own claims with standing, or under a `CLAIM_SIGNER` key granted by the Investor's identity. The digest is marked revoked. Optional.                   |

* **A revoked digest stays revoked.** No function reverses it. To attest the fact again, sign new `ClaimData`.
* **A removal by the Identity Owner spends the digest too.** The digest of the removed claim is marked revoked on the issuer, with `ClaimRemoved` and no `ClaimRevoked`. The same signed claim cannot be delivered again.
* **Revoke what you supersede.** A replaced claim that is neither expired nor revoked is still valid, and the Identity Owner can add it back.

## Reputation and standing

`ReputationRegistry` stores a score per identity (`setReputation`), a default score per identity type (`setDefault`) and one claim-add threshold (`setClaimAddThreshold`). All three are set by the `REPUTATION_MANAGER` role, under Network governance. `reputationOf(identity)` returns the score of the identity if one was set, zero included, and otherwise the default of its type as the factory recorded it.

A Claim Issuer has standing when `reputationOf(issuer)` is at least `claimAddThreshold()` and the factory recorded its identity as `CLAIM_ISSUER`. See [Claim issuers, trust and reputation](/identity-onchainid/claim-issuers-trust-and-reputation.md).

* **It gates** adding the issuer's own claims to an identity where it holds no claim key, and removing them from an identity where it holds no `CLAIM_SIGNER` key.
* **It does not gate** signing, the validity of claims already issued, delivery by the Identity Owner or under a key grant, or revocation by digest. It decides eligibility for no asset.

What to build: read both values before choosing a delivery route, and follow `ReputationSet`, `DefaultSet` and `ClaimAddThresholdSet`.

## Be counted by an asset

A claim counts for an asset once its Issuer has added the Claim Issuer's ONCHAINID, not a signing key, with `TREXRegistry.addTrustedIssuer(trustedIssuer, claimTopics)`: [04 Add a trusted Claim Issuer](/guides/issuer/04.-add-a-trusted-claim-issuer.md).

* **Topics are agreed with each Issuer.** A topic is a number. The registry checks that a valid claim exists for it, never what the payload says.
* **Issuers are advised to trust only factory-created `CLAIM_ISSUER` identities, and adapters governed by Network governance.** For an identity, they check `IdentityFactory.identityTypeOf(issuer)`.
* **Read your status per asset.** `isTrustedIssuer(issuer)` and `getTrustedIssuerClaimTopics(issuer)` on its `TREXRegistry`, and the events `TrustedIssuerAdded`, `TrustedIssuerRemoved` and `ClaimTopicsUpdated`.

## Operations

**Rotate a signing key.** Add the new `CLAIM_SIGNER` key, sign and deliver again every claim of the old key that is still in force, then remove `CLAIM_SIGNER` from the old key. For a compromised key, remove the purpose first: one transaction withdraws everything it signed, forged claims included. Then issue again.

**Store off-chain, per claim:** the Investor's ONCHAINID, the topic, the scheme, the URI, the exact `ClaimData`, the signature, the signing key, the digest and the verification record. Revocation takes the digest, and only the exact data recomputes it.

**Index.** The shared `ERC734Validator` emits these events for every identity. Filter on the indexed fields, in bold.

| Event                                        | Arguments                                                                                            | Meaning                                                                        |
| -------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `ClaimAdded`, `ClaimChanged`, `ClaimRemoved` | **`identity`**, `claimId`, **`topic`**, `scheme`, **`issuer`**, `signature`, `data`, `uri`, `caller` | A claim was added to an identity, replaced, or deleted from it                 |
| `ClaimRevoked`                               | **`digest`**, **`issuer`**, `caller`                                                                 | The issuer revoked a digest                                                    |
| `ClaimAddedTo`                               | **`identity`**, `topic`, `signature`, `data`, `caller`                                               | `addClaimTo` succeeded. It does not name the issuer: pair it with `ClaimAdded` |
| `KeyAdded`                                   | **`account`**, **`keyHash`**, **`purpose`**, `keyType`, `signerData`, `clientData`, `caller`         | A key gained a purpose on the identity `account`                               |
| `KeyRemoved`                                 | **`account`**, **`keyHash`**, **`purpose`**, `caller`                                                | A key lost a purpose                                                           |

**An Investor with no identity.** Read `IdentityFactory.getIdentityIncludingRevoked(account)`, with `account` the wallet as an ERC-7930 interoperable address. If the wallet was never bound, a Claim Issuer that holds the creation role of the Investor's type creates the identity with `createIdentityFor`, with the wallet as its only manager: [02](/guides/identity/02.-deploy-an-onchainid.md).

**Attesters already on EAS.** An attester on the Ethereum Attestation Service needs no ONCHAINID. The `EASClaimIssuer` adapter is the issuer of the claim and answers `isClaimValid` from EAS at every check. The Identity Owner adds the record, and Issuers trust the adapter: [29 Bring an EAS attestation in as a claim](/guides/claim-issuer/29.-bring-an-eas-attestation-in-as-a-claim.md).

## Checklist

* The identity is of type `CLAIM_ISSUER` in the `IdentityFactory`, with signing keys separate from `MANAGEMENT`.
* Every claim is signed over `getClaimHash`, with no personal data in the payload.
* `validUntil` is set wherever the verification has to be renewed.
* The delivery route follows standing, with delivery by the Identity Owner as fallback.
* The digest, the exact data and the signing key of every claim are stored.
* Superseded and withdrawn claims are revoked by digest.
* Claim, key, reputation and trusted-issuer events are indexed.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.t-rex.network/build/integrate-as-a-claim-issuer.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
