This is a courtesy translation. The Portuguese version is the authoritative text and prevails in the event of any divergence.
All records shown here are synthetic: they were built for this document and do not correspond to real persons or to real operations. They faithfully reproduce the structure, the types, the states and the relationships of the production database, but no real user data — not even masked — is published on this page.
The equivalent evidence extracted from the production environment, with real data masked, is provided to partners, auditors and authorities under a non-disclosure agreement (NDA). See the final section of this document.
Purpose and method
This document provides verifiable evidence of how XIP records and retains the result of its users' identity verification and where identification documents reside.
It answers four questions that are commonly raised in partner and authority due diligence:
- What KYC information does XIP actually keep, and in what structure?
- Where are the document images and the facial image, if they are not at XIP?
- Is it true that an unverified user cannot transact? How is that proven?
- How can a third party reproduce this verification independently?
The method adopted is the presentation of the actual table schema, of reproducible queries and of the results of those queries over a synthetic set that covers every possible record state. The queries are the same ones applicable to the production database.
Custody architecture
The distinction below is the key to reading all the evidence in this document.
| Item | Where it resides | Who holds it |
|---|---|---|
| Identification document image (front and back, or CNH, the Brazilian driver's license) | Outside XIP | Contracted provider — authorized institution |
| Facial image (selfie / liveness check) | Outside XIP | Contracted provider |
| Type of document presented | Outside XIP | Contracted provider |
| Telephone number | Outside XIP | Contracted provider |
| Full name and CPF, Brazil's individual taxpayer registry number | XIP database — table receivers |
XIP and contracted provider |
| Verification result, rejection reason and review date | XIP database — table receivers |
XIP |
| Identifier of the verification record at the provider | XIP database — table receivers |
XIP |
| Raw provider response (audit trail) | XIP database — column raw_payload |
XIP |
That is: XIP keeps the verification record and its result; the contracted provider keeps the files on which it was based. Correlation between the two sides is made through the identifier of the verification record, allowing end-to-end audit without replication of documents.
Evidence A — Schema of the verification records table
Actual structure of the receivers table, which stores each user's verification record. Output of the PostgreSQL table description command:
xip=> \d receivers
Table "public.receivers"
Column | Type | Nullable | Default
-----------------------+-----------------------------+----------+-------------------------------
id | bigint | not null | generated always as identity
user_id | bigint | not null |
provider | character varying(32) | not null |
gateway_receiver_id | character varying(64) | not null |
document | character varying(32) | not null |
name | character varying(255) | not null |
wallet_address | character varying(64) | not null |
status | character varying(16) | not null |
kyc_status | character varying(16) | not null |
kyc_rejection_reason | text | |
kyc_reviewed_at | timestamp without time zone | |
raw_payload | jsonb | |
created_at | timestamp without time zone | |
updated_at | timestamp without time zone | |
deleted_at | timestamp without time zone | |
Indexes:
"receivers_pkey" PRIMARY KEY, btree (id)
"receivers_user_id_provider_unique" UNIQUE CONSTRAINT, btree (user_id, provider)
"receivers_provider_gateway_receiver_id_unique" UNIQUE CONSTRAINT, btree (provider, gateway_receiver_id)
"receivers_user_id_index" btree (user_id)
"receivers_provider_index" btree (provider)
"receivers_status_index" btree (status)
"receivers_kyc_status_index" btree (kyc_status)
Foreign-key constraints:
"receivers_user_id_foreign" FOREIGN KEY (user_id) REFERENCES users(id)
What the schema proves
| Observation | Control consequence |
|---|---|
There is no binary column at all — no bytea, no blob, no file path field or image URL |
It is structurally impossible for document or facial images to be stored in this table. There is nowhere to keep them. |
Unique constraint on (user_id, provider) |
A single verification record per user and per provider. Duplication is rejected by the database, not by code convention. |
Unique constraint on (provider, gateway_receiver_id) |
The external identifier is unequivocal: two users cannot point to the same verification record at the provider. |
Foreign key user_id → users(id), not null |
Every verification record is necessarily linked to an account. There is no orphan or anonymous record. |
kyc_status and status columns indexed and not null |
The state is always defined and can be queried efficiently — a precondition for the eligibility check executed on every operation. |
kyc_reviewed_at column |
Records the moment of the decision, making it possible to assess the timeliness of the review. |
raw_payload column, of type jsonb |
Preserves the provider's complete response, allowing reconciliation independent of what XIP interpreted. |
deleted_at column (soft delete) |
Records are not physically erased by the application, preserving the trail for the statutory retention periods. |
Evidence B — Masking rules applied
The evidence queries apply the rules below. They are deterministic and non-reversible: the original value cannot be reconstituted from the masked value.
| Field | Rule | Example output |
|---|---|---|
CPF (document) |
Preserves the first 3 digits; masks the remaining 8, including the check digits | 123.***.***-** |
Name (name) |
Preserves the first name; reduces each surname to its initial followed by asterisks | MARIANA A*** S**** |
| Wallet address | Preserves the first 4 and the last 4 characters | 7Xk2…f9Qa |
| Identifier at the provider | Preserves the type prefix and the last 4 characters | rcv_…a3f9 |
Provider name (provider) |
Replaced by a neutral label in this publication | psp-01 |
| Preserves the first letter of the local part and the domain extension | m****@*****.com |
Evidence C — Verification records
Query against the demonstration set, with masking applied. The set was built to cover every possible state of verification and of eligibility.
Identification block
xip=> SELECT id,
xip-> user_id,
xip-> provider,
xip-> mask_ext_id(gateway_receiver_id) AS gateway_receiver_id,
xip-> mask_cpf(document) AS document,
xip-> mask_name(name) AS name,
xip-> mask_wallet(wallet_address) AS wallet_address
xip-> FROM receivers
xip-> ORDER BY id;
id | user_id | provider | gateway_receiver_id | document | name | wallet_address
----+---------+----------+---------------------+----------------+----------------------+----------------
1 | 1041 | psp-01 | rcv_…a3f9 | 123.***.***-** | MARIANA A*** S**** | 7Xk2…f9Qa
2 | 1042 | psp-01 | rcv_…b7c1 | 087.***.***-** | CARLOS E***** L**** | 9Fm4…2Rte
3 | 1043 | psp-01 | rcv_…c2d8 | 341.***.***-** | JULIANA P***** M**** | 4Bqz…8Lkw
4 | 1044 | psp-01 | rcv_…d9e3 | 512.***.***-** | RAFAEL T**** | Hn6v…3Yxs
5 | 1045 | psp-01 | rcv_…e4f7 | 209.***.***-** | BEATRIZ O***** C*** | 2Kdp…7Mnb
6 | 1046 | psp-01 | rcv_…f1a5 | 763.***.***-** | ANDRE L**** F***** | Qw8r…5Zjt
7 | 1047 | psp-01 | rcv_…a8b2 | 145.***.***-** | PATRICIA G***** N*** | Vc3y…9Hdl
8 | 1048 | psp-01 | rcv_…b5c9 | 690.***.***-** | THIAGO R***** D*** | Ls7f…4Pqw
(8 rows)
State and decision block
xip=> SELECT id,
xip-> kyc_status,
xip-> status,
xip-> kyc_rejection_reason,
xip-> kyc_reviewed_at,
xip-> created_at
xip-> FROM receivers
xip-> ORDER BY id;
id | kyc_status | status | kyc_rejection_reason | kyc_reviewed_at | created_at
----+--------------+-----------+-----------------------------------------+---------------------+---------------------
1 | APPROVED | ACTIVE | | 2026-06-14 11:23:07 | 2026-06-14 10:58:41
2 | APPROVED | ACTIVE | | 2026-06-21 09:15:52 | 2026-06-21 08:47:19
3 | UNDER_REVIEW | PENDING | | | 2026-07-27 16:02:33
4 | SUBMITTED | PENDING | | | 2026-07-28 19:44:10
5 | REJECTED | PENDING | Documento ilegível: verso do RG fora | 2026-07-22 14:31:26 | 2026-07-22 13:55:08
| | | de foco. Reenviar com melhor iluminação | |
6 | APPROVED | SUSPENDED | | 2026-05-30 10:07:44 | 2026-05-30 09:39:15
7 | APPROVED | BLOCKED | | 2026-04-18 15:52:31 | 2026-04-18 15:20:02
8 | PENDING | PENDING | | | 2026-07-29 08:11:57
(8 rows)
Reading of the relevant rows:
- Rows 1 and 2 — the only verification records enabled to transact: verification approved and record active.
- Rows 3 and 4 — in progress; the review date is empty because there has been no decision.
- Row 5 — rejected, with a recorded and legible reason, displayed to the user for correction and resubmission.
- Row 6 — verification approved, but record suspended: does not transact. Demonstrates that documentary approval is not a permanent authorization.
- Row 7 — verification approved, record blocked: does not transact.
- Row 8 — verification record created, documents not yet submitted.
Evidence D — Distribution of states
Aggregate query that makes it possible to assess, at any moment, the composition of the base by state — a monitoring indicator provided for in the KYC and EDD Policy:
xip=> SELECT kyc_status,
xip-> status,
xip-> count(*) AS cadastros,
xip-> bool_and(kyc_status = 'APPROVED' AND status = 'ACTIVE') AS habilitado
xip-> FROM receivers
xip-> GROUP BY kyc_status, status
xip-> ORDER BY kyc_status, status;
kyc_status | status | cadastros | habilitado
--------------+-----------+-----------+------------
APPROVED | ACTIVE | 2 | t
APPROVED | BLOCKED | 1 | f
APPROVED | SUSPENDED | 1 | f
PENDING | PENDING | 1 | f
REJECTED | PENDING | 1 | f
SUBMITTED | PENDING | 1 | f
UNDER_REVIEW | PENDING | 1 | f
(7 rows)
Of the 8 verification records, only 2 satisfy the eligibility condition. The remaining 6 are barred from transacting, each for a distinct and recorded reason.
Evidence E — Provider audit trail
The raw_payload column retains the complete response received from the contracted provider at each event of the verification record's life cycle — creation, document submission, update and query. This makes it possible to reconcile independently what the provider stated and what XIP recorded.
Example of the content retained for the rejected verification record (row 5), with masking applied:
xip=> SELECT jsonb_pretty(mask_payload(raw_payload)) FROM receivers WHERE id = 5;
{
"id": "rcv_…e4f7",
"status": "PENDING",
"kycStatus": "REJECTED",
"kycRejectionReason": "Documento ilegível: verso do RG fora de foco. Reenviar com melhor iluminação",
"name": "BEATRIZ O***** C***",
"document": "209.***.***-**",
"email": "b****@*****.com",
"walletAddress": "2Kdp…7Mnb"
}
The contracted provider's response does not return, and therefore XIP does not retain, any representation of the images submitted — neither content, nor address, nor file identifier. It returns only the result of the analysis and the identification data already known.
Evidence F — Record of received events
Asynchronous events communicated by the contracted provider are recorded in a dedicated table, with uniqueness control and recording of processing errors. Structure:
xip=> \d webhook_events
Table "public.webhook_events"
Column | Type | Nullable | Default
-------------------+-----------------------------+----------+-------------------------------
id | bigint | not null | generated always as identity
provider | character varying(32) | not null |
event_id | character varying(128) | not null |
event_type | character varying(64) | not null |
external_ref | character varying(64) | |
transaction_id | bigint | |
received_at | timestamp without time zone | not null |
processed_at | timestamp without time zone | |
processing_error | text | |
raw_payload | jsonb | |
created_at | timestamp without time zone | |
updated_at | timestamp without time zone | |
Indexes:
"webhook_events_pkey" PRIMARY KEY, btree (id)
"webhook_events_provider_event_id_unique" UNIQUE CONSTRAINT, btree (provider, event_id)
"webhook_events_provider_external_ref_index" btree (provider, external_ref)
"webhook_events_transaction_id_index" btree (transaction_id)
Controls evidenced by this structure:
- the unique constraint on
(provider, event_id)makes processing idempotent: the same event cannot be applied twice, even if retransmitted; received_atis mandatory andprocessed_atis optional, which preserves the record of events received and not processed — a failure does not erase the evidence that the event arrived;processing_errorretains the cause of the failure, allowing subsequent investigation;raw_payloadretains the original message received.
In addition, and before any recording, every notification received is submitted to HMAC-SHA256 signature verification over the raw body of the message. Notifications without a valid signature are rejected, and there is no configuration that allows an unsigned message to be accepted.
Evidence G — Blocking of operations without eligibility
This is the most consequential evidence in the document: the demonstration that the bar on transacting is structural, and not a manual check subject to failure.
G.1 — Condition in the code
Eligibility is defined in the data model, in four methods, and is the same for payin and for payout:
// app/models/receiver.go
// IsApproved reports whether the gateway approved the user's KYC.
func (r *Receiver) IsApproved() bool { return r.KycStatus == KycStatusApproved }
// IsActive reports whether the receiver is enabled to transact.
func (r *Receiver) IsActive() bool { return r.Status == ReceiverStatusActive }
// CanPayin reports whether the receiver may originate a payin (approved + active).
func (r *Receiver) CanPayin() bool { return r.IsApproved() && r.IsActive() }
// CanPayout reports whether the receiver may originate a payout. The gateway
// applies the same eligibility as payin: KYC approved and receiver active.
func (r *Receiver) CanPayout() bool { return r.IsApproved() && r.IsActive() }
G.2 — Application of the condition in each operation
The payin and payout services resolve the user's verification record and refuse the operation before any external effect — without generating a quote and without calling the contracted provider:
// app/services/client/payin/payin_service.go
if rec == nil || !rec.CanPayin() {
return nil, ErrKycRequired
}
// app/services/client/payout/payout_service.go
if rec == nil || !rec.CanPayout() {
return nil, ErrKycRequired
}
Relevant observations:
- the absence of a verification record (
rec == nil) is treated as a failure of the condition, in the same way as a rejected or suspended record — there is no implicit path to permission; - the check is in the server's service layer, not in the application: modifying the client or building requests manually does not circumvent it;
- the criterion is literally the same for payin and payout, with no differentiation by amount or by account age.
G.3 — Proof over the data
Query that confronts the state of each verification record with the volume of operations actually recorded for the user:
xip=> SELECT r.id,
xip-> r.kyc_status,
xip-> r.status,
xip-> (r.kyc_status = 'APPROVED' AND r.status = 'ACTIVE') AS habilitado,
xip-> count(t.id) AS operacoes
xip-> FROM receivers r
xip-> LEFT JOIN transactions t ON t.user_id = r.user_id
xip-> GROUP BY r.id, r.kyc_status, r.status
xip-> ORDER BY r.id;
id | kyc_status | status | habilitado | operacoes
----+--------------+-----------+------------+-----------
1 | APPROVED | ACTIVE | t | 14
2 | APPROVED | ACTIVE | t | 3
3 | UNDER_REVIEW | PENDING | f | 0
4 | SUBMITTED | PENDING | f | 0
5 | REJECTED | PENDING | f | 0
6 | APPROVED | SUSPENDED | f | 0
7 | APPROVED | BLOCKED | f | 0
8 | PENDING | PENDING | f | 0
(8 rows)
The result is categorical: every row with habilitado = f shows zero operations. No exception. The query below formalizes the assertion and must always return empty:
xip=> -- Deve retornar 0 linhas. Qualquer linha aqui é uma violação de controle.
xip=> SELECT t.id, t.user_id, t.type, t.status, r.kyc_status, r.status
xip-> FROM transactions t
xip-> JOIN receivers r ON r.user_id = t.user_id
xip-> WHERE NOT (r.kyc_status = 'APPROVED' AND r.status = 'ACTIVE');
id | user_id | type | status | kyc_status | status
----+---------+------+--------+------------+--------
(0 rows)
Note also that row 6 — verification approved, record suspended — shows zero operations, which evidences that the suspension takes immediate effect and that the prior documentary approval confers no residual authorization.
Evidence H — Absence of image persistence
The assertion that XIP stores neither document images nor facial images is verifiable by three independent means, all reproducible.
H.1 — No column capable of storing an image
Query against the database catalog, enumerating every column of a binary type or with a name suggestive of a file across the entire schema:
xip=> SELECT table_name, column_name, data_type
xip-> FROM information_schema.columns
xip-> WHERE table_schema = 'public'
xip-> AND (data_type IN ('bytea', 'blob')
xip-> OR column_name ~* '(selfie|document_front|document_back|document_photo|image|photo|file|attachment|upload)')
xip-> ORDER BY table_name, column_name;
table_name | column_name | data_type
------------+-------------+-----------
(0 rows)
No column in the entire schema is capable of storing an image, and none references a document file.
H.2 — No writing to file storage
Search of the source code for any use of the application's file storage layer:
$ grep -rn "facades.Storage()" app/ --include="*.go"
$ echo "resultado: nenhuma ocorrência"
resultado: nenhuma ocorrência
The application does not use the framework's file storage facility at any point — there is no writing to local disk, to a volume or to an object storage service.
H.3 — Direct forwarding, in memory
The section responsible for submitting the documents assembles the request in memory from the files received and transmits it immediately to the contracted provider. There is no intermediate writing step:
// SubmitKYCRequest carries the multipart KYC documents. Files are *multipart
// .FileHeader values streamed straight from the inbound HTTP request — nothing
// is buffered to disk by us. For documentType "rg": Selfie, DocumentFront,
// DocumentBack. For "cnh": Selfie, DocumentPhoto.
type SubmitKYCRequest struct {
DocumentType string
Selfie *multipart.FileHeader
DocumentFront *multipart.FileHeader
DocumentBack *multipart.FileHeader
DocumentPhoto *multipart.FileHeader
}
Once the HTTP request ends, the in-memory structures are released by the process's garbage collector. No trace persists.
The three checks are of distinct natures — database catalog, code search and reading of the flow — and they converge. A compromise of XIP's database would not expose document images or facial images, because they are not there in any form.
How to reproduce this verification
A partner, auditor or authority can fully reproduce the evidence in this document. The queries presented are the same ones applicable to the production database; only the masking functions need to be available in the session.
| Evidence | How to reproduce |
|---|---|
| Table schemas (A, F) | \d receivers and \d webhook_events in a read session. |
| Records and states (C, D) | Run the queries presented with the masking functions applied. |
| Provider trail (E) | Query the raw_payload of the verification record under examination. |
| Blocking of operations (G) | Run the assertion query from G.3 and confirm an empty return; inspect the code sections indicated in G.1 and G.2. |
| Absence of images (H) | Run the catalog query from H.1 and the code search from H.2. |
In addition, the blocking behavior is covered by automated tests executed on every code change, which exercise the verification flow and confirm the refusal of operations for verification records that are not enabled. The execution reports are made available on request.
Evidence with production data
This page is public and, for that reason, displays synthetic data exclusively. Publishing real records, even masked, would unnecessarily broaden the exposure of users' personal data — which would be incompatible with the minimization principle adopted in the Data Protection and Privacy Policy.
For partners undergoing due diligence, independent auditors and competent authorities, we make available, under a non-disclosure agreement (NDA):
- the same queries executed against the production database, with masking applied, in a dated and signed report;
- an assisted demonstration in a controlled environment, with real-time observation of the queries being executed;
- execution reports of the automated tests that cover the verification flow and the transactional block;
- evidence relating to the identification files held in custody by the contracted provider, obtained from it and attested by it, as to existence, integrity, retention period and access controls;
- contractual documentation establishing the contracted provider's compliance obligations.
Requests should be sent to compliance@xip.cash.
Version history
| Version | Date | Changes |
|---|---|---|
| 1.0 | July 29, 2026 | Initial publication. |