Consuming Attesta

Release scope

The supported integration is a hosted REST flow. A customer does not need to modify a smart contract and does not query a public wallet KYC registry. Protocols with an existing Attesta adapter may opt into the explicit, server-only post-approval publication step described below.

From the customer's perspective the protocol is:

existing authenticated session
        |
        +-- wallet signs one short-lived binding
        |
customer backend -- POST /v1/flows --> Attesta hosted page --> Didit if needed
        ^                                      |
        +----------- signed webhook -----------+

approved protocol flow -- POST /v1/flows/{requestId}/attestation --> adapter

The customer calls one Attesta creation endpoint. The hosted page's exchange, session, provider-launch, and polling routes are Attesta internals and are not part of the customer API.

Provisioned values

Attesta provisions each customer with:

Tenant records are database-backed. A new tenant or rotated credential becomes active without an Attesta restart or a per-tenant environment-variable edit. The human-readable tenant name is control-plane metadata only; the opaque ID is hashed into wallet authorizations and the display name is never copied into a verification record or webhook.

1. Create the wallet binding

Generate a cryptographically random 32-byte requestId and persist this local mapping before calling Attesta:

requestId -> your account/profile and expected wallet

Build this EIP-712 message from the already authenticated account:

Domain
  name:    Attesta KYC
  version: 1
  chainId: <configured EVM chain>

Primary type: AttestaHostedKycBinding
  bytes32 protocolVersion
  bytes32 tenantIdHash
  bytes32 requestId
  bytes32 policyId
  uint8   authorizationMode
  address signer
  address businessWallet
  uint256 chainId
  uint64  deadline

Derived constants:

protocolVersion = keccak256("AttestaHostedKycBinding/v1")
tenantIdHash     = keccak256(utf8(lowercase opaque tenantId))

Authorization modes:

Value JSON name Rule
1 direct_wallet signer == businessWallet
2 tenant_mapped_account the established authentication signer authorizes the business wallet recorded by the tenant's standardized account system

Do not accept the wallet, tenant, policy, chain, or return URL from unauthenticated browser input. Do not start another wallet login. Use the same authenticated wallet adapter and account-to-business-wallet mapping already used by the application.

Set deadline no more than the configured authorization TTL into the future (five minutes by default). The user signs this typed request once.

2. Call the one creation endpoint

POST /v1/flows
X-API-Key: <server-only tenant key>
Content-Type: application/json
{
  "full_name": "Joana Silva",
  "request_id": "0x1111111111111111111111111111111111111111111111111111111111111111",
  "policy_id": "0x525ddd7695cee6e297fc4ac870b49aeff1aeeaac3efa60b1ac00b5f9d9f89e2f",
  "chain_id": 43113,
  "authorization_mode": "direct_wallet",
  "signer_address": "0x1234567890abcdef1234567890abcdef12345678",
  "wallet_address": "0x1234567890abcdef1234567890abcdef12345678",
  "deadline": 1800000000,
  "signature": "0x<65-byte-signature>"
}

Attesta returns exactly:

{
  "flow_id": "7c4ff7ccda51463f95fbfbdc438e5ec8",
  "hosted_url": "https://kyc.example/verify/<one-time-token>",
  "expires_at": "2026-08-10T12:00:00Z"
}

Validate the configured HTTPS origin and /verify/ path, then open hosted_url. Do not parse identity or authority from the token. It is a short-lived browser capability; only its digest is stored, and its exchange is atomic and one-time.

This request intentionally contains no email, phone, CPF, document number, external account reference, or customer account ID. The wallet and signature are transient authorization inputs. Attesta verifies them and persists only a keyed wallet tag.

3. First verification versus reuse

For a wallet with no active credential under the requested policy and chain, the hosted page starts Didit. Didit collects the document, selfie, liveness, and face-match evidence on its origin.

Attesta approves only after it:

  1. validates Didit's signed callback and timestamp;
  2. fetches the authoritative decision server-to-server;
  3. confirms the expected workflow, session, document, liveness, and face-match results;
  4. extracts exactly one provider-verified legal name plus any verified birth date and nationality/issuing country, then screens those values transiently; and
  5. completes sanctions/PEP decisioning.

A missing or ambiguous verified identity does not pass automatically; it enters review. For a successful first verification, the authenticated status response delivers the verified name temporarily so the tenant can correct its local display name. The hosted browser and webhook never receive it.

For a matching active private credential, Attesta skips Didit and immediately issues the new tenant's opaque grant. This works across approved tenants because the wallet tag is policy-scoped, not tenant-scoped. The new tenant learns only the result for its own requestId; it cannot list the other tenants or query a public wallet status.

4. Consume the signed webhook

Attesta signs the exact raw JSON body with the tenant webhook secret:

X-KYC-Signature = hex(HMAC-SHA256(webhookSecret, rawBody))

Example:

{
  "event": "case.approved",
  "data": {
    "request_id": "0x1111111111111111111111111111111111111111111111111111111111111111",
    "policy_id": "0x525ddd7695cee6e297fc4ac870b49aeff1aeeaac3efa60b1ac00b5f9d9f89e2f",
    "status": "approved",
    "credential_expires_at": 1810000000
  }
}

Receiver requirements:

  1. Read the raw body before JSON parsing.
  2. Recompute the HMAC and compare it in constant time.
  3. Require the locally expected request_id and policy_id.
  4. Apply only allowed monotonic state transitions.
  5. Make repeated identical events idempotent.
  6. Return success only after the local request state is durable.

Events are durably queued by Attesta and retried with a worker lease and bounded backoff. Approval, rejection, expiry, and revocation use the same minimal data shape. When a credential is suspended, every tenant grant created from it gets its own case.suspended event with only that tenant's request ID.

5. Optional status reconciliation

If a webhook is delayed, query the same opaque request:

GET /v1/flows/0x<request-id>
X-API-Key: <server-only tenant key>
{
  "request_id": "0x<request-id>",
  "status": "approved",
  "credential_expires_at": 1810000000,
  "verified_full_name": "Maria Souza"
}

The API key is tenant-scoped. A different tenant receives 404 even if it guesses the request ID. verified_full_name is present only for the bounded post-Didit delivery window and is then null; credential reuse also returns null because no new document name was processed.

Status handling

Status Customer behavior
created hosted flow exists but verification has not started
pending / in_review keep access restricted and show the hosted/review state
approved enable the customer's KYC-dependent application behavior until expiry
rejected keep access restricted; show reason-safe retry/support copy
suspended remove KYC-dependent access and allow a new verification when policy permits

The customer remains authoritative for its own account state. Browser popup messages are refresh hints only; they must never mark an account approved.

Optional protocol-attestation step

Only after the authenticated status is approved, a protocol backend may call:

POST /v1/flows/0x<request-id>/attestation
X-API-Key: <server-only tenant key>
Content-Type: application/json

{"wallet_address":"0x<expected-business-wallet>"}

Attesta derives the wallet tag again from the flow's policy and chain and compares it to the approved private credential. Therefore the caller cannot substitute a different wallet. Publication is idempotent: the signed attestation is persisted before the chain side effect, and retries publish or verify those same values. The response is approved only after the registry write and configured adapter view are confirmed.

Data retained by Attesta

After terminal processing, the reusable credential contains:

policyId
chainId
HMAC wallet tag + key version
active/revoked/expired status
expiry
opaque revocation handle
timestamps

The tenant grant contains:

opaque tenant ID
requestId
policyId
credential reference
grant status and expiry
timestamps

It contains no plaintext wallet, name, email, customer account reference, signature, CPF, document, selfie, or provider report. Temporary identity and provider-session data exist only while the first KYC is being reconciled and are erased after the terminal result is queued. An abandoned request that never starts provider verification is deleted after its launch and hosted session capabilities have both expired.

The optional protocol-attestation step has a distinct, explicit privacy boundary: its claim subject is the business wallet, which is retained in the attestation record and published on-chain. The private hosted credential and tenant grant remain pseudonymous.

The HMAC wallet tag is pseudonymous, not anonymous. Its security depends on a secret MAC key and restricted access to the MAC operation. Didit necessarily processes the user's evidence while performing KYC. These limits must be stated in privacy notices and the data-processing agreement.

Errors and retries

Retry 503 with bounded backoff. Repeating an identical unstarted request ID rotates its one-time launch capability; changing any authority-bearing field for that request fails closed.

What is not required

An official thin adapter may package typed-data construction, URL validation, and webhook verification, but it does not change this wire contract.