CorePass
Start integrating

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.

10 minute read · Updated 2026-09-05


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? #

POST /api/v1/blockchain/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"]
    }'
200 OK
{
  "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.

POST /api/v1/kyc/qrcode
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.
201 — first request
{
    "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:

Subsequent requests
{
    "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:

In your page
<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.

POST to your statusCallback
{
  "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.

POST to your callback
{
  "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:

infos, decoded once
[
  {
    "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:

POST /api/v1/kyc/status
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"]
    }'
200 OK
{
	"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:

POST /api/v1/blockchain/valid
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"
    }
  ]
}'
200 OK
{
  "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.