Retrieving KYC data
Check what a person has verified, request the fields you need, follow the transfer through its statuses, and read the values out of the callback.
A KYC transfer moves verified identity data from the person who owns it to the application that asked for it, in exchange for Core Token — and the token transfer is handled for you. Your side is four calls and one endpoint: check what the person has verified, request the fields, follow the status, and read the values out of the callback.
Prerequisites #
- The person has completed the KYC verification in their CorePass application. Nothing can be requested that they have not verified.
- You hold their Core ID, which means they have signed in to your platform.
- You can serve two HTTPS endpoints — one for the data, one for the status — and answer them with a 200.
The field vocabulary #
Decide what you need before you write any code: you request fields by name,
and the names are the vocabulary below. Four document types carry the same
sixteen fields each, prefixed
IDCARD_,
PASSPORT_,
RESIDENCE_PERMIT_ and
DRIVER_LICENSE_:
NAME | NAME_EN | DOB | EXPIRY_DATE |
ISSUE_DATE | DOCUMENT_NUMBER | SEX | COUNTRY |
NATIONALITY | PLACE_OF_BIRTH | DOCUMENT_IMAGE | FACE_IMAGE |
ADDITIONAL_IMAGE | AML_CHECK | AML_DETAIL | NOTE |
So a passport date of birth is
PASSPORT_DOB and a driver's licence document
number is DRIVER_LICENSE_DOCUMENT_NUMBER.
Address is its own set:
ADDRESS_NAME | ADDRESS_NAME_EN | ADDRESS_ISSUE_DATE | ADDRESS_EXPIRY_DATE |
ADDRESS_STREET | ADDRESS_STREET_EN | ADDRESS_STREET2 | ADDRESS_STREET2_EN |
ADDRESS_DIVISION | ADDRESS_DIVISION_EN | ADDRESS_CITY | ADDRESS_CITY_EN |
ADDRESS_ZIP_CODE | ADDRESS_COUNTRY | ADDRESS_DOCUMENT_IMAGE | ADDRESS_NOTE |
Two more stand alone: EMAIL and
PHONE.
The _EN suffix #
A field with the _EN suffix is the value in
English; the same field without it is the value in the person's own
language. Ask for both and you get both; ask for one and you get one. If
the person's own language is English the two are identical.
Document type matters more than you expect. If you only
want a date of birth and do not care which document it came from, you
still have to check which of
IDCARD_DOB,
PASSPORT_DOB,
RESIDENCE_PERMIT_DOB and
DRIVER_LICENSE_DOB this person actually has,
and ask for that one. Asking for a field they have not verified is an
error, not an empty answer.
1. What has the person verified? #
curl --request POST \
--url https://auth-develop.corepassdev.eu/api/v1/blockchain/verified \
--header 'Content-Type: application/json' \
--data '{
"user":"ab22b1671b4f7ccc0b16a87514adde84513b6348232e",
"items": ["IDCARD_DOB", "DRIVER_LICENSE_DOCUMENT_NUMBER","EMAIL"]
}' {
"verifiedItems": ["EMAIL"],
"unVerifiedItems": ["IDCARD_DOB", "DRIVER_LICENSE_DOCUMENT_NUMBER"]
}
A person who has verified nothing you asked for comes back with an
unVerifiedItems list and no
verifiedItems member at all. If a field your
application depends on is unverified, tell the person to complete that
verification in their CorePass application — until they do, the transfer
cannot be started.
2. Request the transfer #
This is the central call. It returns the QR code you show the person, and it is where you name the callbacks and the deadline.
curl --request POST \
--url https://auth-develop.corepassdev.eu/api/v1/kyc/qrcode \
--header 'Content-Type: application/json' \
--data '{
"user":"ab72a31c718d343b45e558099ec503087f734433785d",
"items":["DRIVER_LICENSE_DOCUMENT_NUMBER"],
"callback":"d291c80a-27e3-4c18-af69-5ecb5014ac3f.webhook.site",
"statusCallback":"2651b29d-2e7f-4b4c-9a6b-702555089ea9.webhook.site",
"expiration":1675946886
}' | Parameter | What it is |
|---|---|
user | The person's Core ID. |
items | The fields you need — the same list you just confirmed as verified. |
callback | Where the data is delivered once the person has approved the request. Called with the data, or with an error. |
statusCallback |
Where status changes are delivered, as a POST with an
application/json body.
|
expiration | A Unix timestamp in seconds, and it must fall between 5 and 15 minutes from now. Anything else is a 400. This is the window the person has to approve the request after scanning. |
withoutQRCode |
Optional. Set it to true when you only
need the link, and the response omits the base64 image.
|
{
"qrcode": BASE64_ENCODED_QR_CODE,
"link": "url to be shown on a button for mobile browsers",
"expiration": 1675946886
} Ask a second time for the same person and items and you get the same code back, flagged:
{
"qrcode": same BASE64_ENCODED_QR_CODE as the first response,
"link": same url as the first response,
"expiration": same expiration as the first response,
"alreadySent": true
} Show the code exactly as you showed the login one:
<img alt="{{.link}}" src=" data:image/png;charset=utf-8;base64,{{.qrcode}}" /> Three refusals worth handling explicitly:
| Status | Body |
|---|---|
| 400 | expiration time is invalid, it should be unix time in seconds and between 5-15 minutes |
| 400 | user does not have the requested items verified |
| 400 | there is an ongoing request for this user and items |
An ongoing request is one that reached
INITIATE_SUBMITTED and has not yet reached
FINISH_SUCCESS or
FINISH_FAILED.
3. Follow the statuses #
Each status change is POSTed to your
statusCallback, signed with the
Corepass-Signature header — see
webhooks for how to verify it.
{
"event": "status.updated",
"timestamp": 1677255437,
"data": {
"userAddress": "ab148af5f9cdad10beddb05fbec4a3bef02577130e56",
"fields": [
"21148787cbc13cfc8acde462c822ff431af96aa2edf714fdc82f90e4c2918824"
],
"status": "initiate_submitted",
"deadline": 1677255497,
"txHash": "0xd90eb185877e47238380f613e3ac77f4cdb6a293ae21019fea7ac1b9ce12a94e",
"timestamp": 1677255437
}
} The status is lower case on the wire. You receive
initiate_submitted, not
INITIATE_SUBMITTED; the table below is upper
case because that is how the statuses are named everywhere else. Compare
case-insensitively. There is no signature member in the body
and no expiration member — authenticity is the header.
txHash is always present and is an empty string
for a status that has no transaction yet. You will see two different hashes
over one transfer: one for the transaction that starts it, and one for the
transaction that finalises it and pays the person.
| Status | Meaning |
|---|---|
PENDING | The QR code or link was generated and the person has not scanned it yet, or has scanned it but not approved the request. |
ACCEPTED | The person scanned the code or tapped the link and approved the data transfer. |
INITIATE_SUBMITTED | The request has been sent to start the transfer. |
INITIATED | The request went through and the transfer has started. |
INITIATED_FAILED | It failed to start. Terminal — your callback is called with the reason. |
KYC_RECEIVED | The person's application was notified, and the raw data reached CorePass Connector. |
VALIDITY_CHECK | The connector received the data and is checking it. |
VALIDITY_SUCCEED | The data is valid. |
VALIDITY_FAILED | The data is not valid. Terminal. |
CONFIRM_SUBMITTED | A confirm transaction was sent to pay the person for the transfer. |
CONFIRMED | The transfer is complete, and your callback is called with the data. |
CONFIRM_FAILED | The confirm transaction failed. Terminal. |
CALLBACK_SUCCEED | Your callback was called successfully. |
CALLBACK_FAILED | Your callback was called and did not answer 200. |
FINISH_SUCCESS | The transfer completed and the person was paid. |
FINISH_FAILED | The transfer failed outright. Terminal. |
A failed transfer reaches you as a status callback carrying the failure
status, in the same status.updated shape — not
as a separate body with an error member. Treat the terminal statuses above
as final, and read the status itself as the reason.
4. Read the data callback #
When the transfer completes, your callback URL
is called with a POST whose body is JSON — not
multipart/form-data — signed with the same
header.
{
"event": "data.transferred",
"timestamp": 1667137346,
"data": {
"userAddress": "ab432e666932c53128d9f73712b058a7a8f7df52f5cb",
"infos": "W3siZmllbGQiOiJEUklWRVJfTElDRU5TRV9ET0IiLCJkYXRhIjoiTVRrNE5pMHdPUzB3Tmc9PSIsInBlcHBlciI6Ik16YzBaVGMwIn1d",
"deadline": 1667137346,
"txHash": "0x1332a0079b54bace370e216f32bb4284adb7a4852c47a71908ec2c6152145114",
"timestamp": 1667137346
}
} data.infos is
base64 of a JSON array, one entry per requested field:
[
{
"field": "DRIVER_LICENSE_DOB",
"data": "MTk4Ni0wOS0wNg==",
"pepper": "Mzc0ZTc0MjY0ZDRiMzE1YTM4Nzg2NTI1NzY2OTZlNTIzOA=="
}
] data and pepper are
themselves base64. So reading a value is: base64-decode
infos, parse the JSON array, then base64-decode
each entry's data.
Save the pepper. It is the value used to fingerprint that field on the registry, it is different for every field of every person, and it is as sensitive as the value itself. Without it you cannot check the value again later — and each request costs you CTN, so losing it means paying twice.
Answer 200. Anything else is treated as a failure and the callback is retried on the intervals your deployment is configured with; when the retries are exhausted, the data sits in the connector's database and has to be exported by hand.
Polling, if you prefer #
Two complementary endpoints exist for the same information, and neither is
mandatory. /api/v1/kyc/status gives you the
latest status for a person and a field list:
curl --request POST \
--url https://auth-staging.corepassdev.eu/api/v1/kyc/status \
--header 'Content-Type: application/json' \
--data '{
"user":"ab29abcf6455efb099ebe820f50d67b96f540e935fa6",
"items":["DRIVER_LICENSE_DOCUMENT_NUMBER"]
}' {
"status": "CONFIRM_SUBMITTED",
"created_at": 1677255348,
"tx_hash": "0x1332a0079b54bace370e216f32bb4284adb7a4852c47a71908ec2c6152145114"
} /api/v1/kyc/all-statuses takes the same body and
returns every status instead of the latest, newest first, under an
AllStatuses member. Both endpoints return
results for the past month only.
Re-checking a value later #
CorePass always hands you valid data, but a value can expire or stop being true afterwards. To check one you already hold, send it back with its pepper:
curl --location 'https://auth-staging.corepassdev.eu/api/v1/blockchain/valid' \
--header 'Content-Type: application/json' \
--data '{ "user": "ab701b918efb6289a5077f6510740b4bca7f707dbad7",
"infos": [
{
"fieldID": "ADDRESS_CITY",
"fieldValue": "Bandar-Abbas",
"pepper": "72b69fbed02d5acbe4aa58d38be4e32283952edc56e9338c4db57bc0ea9b8d4a"
},
{
"fieldID": "ADDRESS_COUNTRY",
"fieldValue": "IRN",
"pepper": "47aa3e25b647f47488d429d4cf3cd21d927f89d7e9f25c060c7d915d3c901123"
}
]
}' {
"valids": ["ADDRESS_COUNTRY"],
"invalids": ["ADDRESS_CITY"]
} This is the call the pepper exists for, and it needs nothing from the person: you are asking the registry whether the fingerprint of the value you hold still matches.