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:
- an opaque
tenantId; - a server-only API key with
flows:createandflows:readscopes; - a server-only webhook secret;
- an approved
policyId; - an exact return URL and webhook URL.
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:
- validates Didit's signed callback and timestamp;
- fetches the authoritative decision server-to-server;
- confirms the expected workflow, session, document, liveness, and face-match results;
- extracts exactly one provider-verified legal name plus any verified birth date and nationality/issuing country, then screens those values transiently; and
- 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:
- Read the raw body before JSON parsing.
- Recompute the HMAC and compare it in constant time.
- Require the locally expected
request_idandpolicy_id. - Apply only allowed monotonic state transitions.
- Make repeated identical events idempotent.
- 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
400: malformed, expired, or internally inconsistent wallet authorization.401: missing/invalid tenant API key or wallet signature.403: requested policy is not enabled for the tenant.404: unknown or cross-tenant request.409: an existingrequest_idwas reused with different identity context.503: required Attesta/provider/key configuration is unavailable.
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
- no Attesta SDK is mandatory;
- no customer smart-contract modification is required;
- no public
canTransact(wallet)lookup is used; - no email or customer account ID is sent to Attesta;
- no second wallet login is introduced.
An official thin adapter may package typed-data construction, URL validation, and webhook verification, but it does not change this wire contract.