Skip to main content

Before On Chain Verify Solution

Business Scenario

This API is for verifying a transfer transaction that requires travel rule verification. The originator should call this API before withdrawal. The GTR central service will save the verified data from the originator request address verification to the beneficiary. Once the central service verifies the address info to the beneficiary, it will transfer the originator's PII via notify callback API to the beneficiary VASP. Finally, the beneficiary's business service will receive the originator's PII with the beneficiary VASP callback API.

Figure 5-1. Before On-Chain Flow.

Figure 5-1. Before On-Chain Flow.

Step-by-step solution flow

Let's assume you are VASP A, and you want to interact with VASP B by following Before On Chian Verify Spec, You will send the Personally Identifiable Information (PII) to VASP B, At the same time, you put PII and both personal addresses to GTR when calling the before-on-chain api. First, GTR will confirm to VASP B that the personal address exists in the VASP B's system. GTR will send the PII to VASP B later, and after all is done, you will receive the transaction ID (txId) from the blockchain. You will put the txId binding to the GTR system and notify that txId to VASP B.

Since PII is encrypted data, and that raw data uses VASP B's public key to encrypt PII data, you have to get VASP B's public key first.

Definition:

  • Originator: VASP A - You
  • Beneficiary: VASP B - Other People

Now, The example below will follow this action:

VASP A sends before on chain verification to VASP B via GTR submit API (with encrypted PII).

VASP B will verify the address exists, and the response will tell GTR the existence or not. If it exists GTR will immediately send the PII as another callback entry to VASP B.

Once VASP A finishes a transaction on the chain, VASP A will update txId to notify VASP B via GTR API.

Which process flow as role you need to watch:

  • Originator - VASP A

    • API 1: list
    • API 2: submit
    • API 4: update txId
  • Beneficiary - VASP B

    • API 1: list
    • API 2: verify
    • API 3: receive PII
    • API 4: listen for txId update notification
  • You must implement both Originator and Beneficiary to complete the integration.

Common API 1: list

To know who is available on GTR service, save the list (vasp_code, vasp_name, company_name) to your database. Once transactions occur from your service, you only need to map other people's VASP code to your system.

Invoke this API to get all VASP exists in GTR by calling the following api:

GET https://platform.globaltravelrule.com/api/vasp/list

For further detail on VASP list api, Please see:

/api/vasp/list

curl --silent --location --request GET "https://platform.globaltravelrule.com/api/vasp/list" \
--cert-type P12 --cert ./certificate.p12:'[MY_PASSWORD_OF_CERT]' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer [YOUR LOGIN TOKEN]" \
--header "Connection: keep-alive"

After you request the VASP list, you may get the following response:

{
"data": [
{
"companyName": "VASP A - Your Company",
"publicKey": "curve25519_pub_vasp_a",
"vaspCode": "vasp_a",
"vaspName": "VASP A - Your Company"
},
{
"companyName": "VASP B - Other People's Company",
"publicKey": "curve25519_pub_vasp_b",
"vaspCode": "vasp_b",
"vaspName": "VASP B - Other People's Company"
}
],
"msg": "success",
"status": "0",
"success": true
}
  • Keep in mind that the public key for the VASP list is not permanent, as people can change it anytime. Do not save it when you invoke this api.

This API should let your service know the all available VASPs on the GTR server.

Originator API 2: submit

For submit, assume a USER A wants to initiate a transaction

Transaction Details:

  • Username form VASP A's service: USER A
  • PII: identificationNumber: ABC1234567
  • Address: 488P2TvBRnn2WFdtGQgTZUB8DuPbeKafCf
  • Ticker: BTC
  • Target VASP to verify: VASP B
  • Network: ETH
  • FiatName: USD
  • FiatPrice: 1.23
  • Amount: 1.23
  • Law Threshold: true

Now, we're currently interested in VASP B's public key, which is the Curve 25519 encryption key for the next process; however, we won't use the VASP list API and execute a complicated search to get the VASP B's info.

Since we know the VASP B's VASP code is vasp_b, we recommend using the following API to directly get VASP B's public key.

To get the target VASP's public key from the detail API:

GET https://platform.globaltravelrule.com/api/vasp/detail

For further detail of VASP detail API, see: /api/vasp/detail

curl --location --request GET "https://platform.globaltravelrule.com/api/vasp/detail?vaspCode=vasp_b" \
--cert-type P12 --cert ./certificate.p12:'[MY_PASSWORD_OF_CERT]' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer [YOUR LOGIN TOKEN]" \
--header "Connection: keep-alive"

And you will get the response:

{
"data": {
"companyName": "VASP B - Other People's Company",
"publicKey": "curve25519_pub_vasp_b",
"vaspCode": "vasp_b",
"vaspName": "VASP B - Other People's Company",
"contactInfo": "xxxx@xxxx.com",
"publicKey": "curve25519_pub_vasp_b",
"registrationAuthorityCountry": "AF"
},
"msg": "success",
"status": "0",
"success": true
}

See the data list; the public key is curve25519_pub_vasp_b.

  • You have to invoke this API every time before you submit a verification request.

Now we're going to build a verification payload to initiate submission.

Prepare to submit the before on chain, using this API to send the payload:

POST https://platform.globaltravelrule.com/api/verify/submit

For further details on the submit verify API, see: /api/verify/submit

  • This API is synchronized, the request will be pending until VASP B finishes all process flow and returns the result. Therefore, please increase the request timeout limit.
# Set your desired length for the random string
length=12

# Generate the random string
random_request_id=$(openssl rand -base64 48 | tr -dc 'a-zA-Z0-9' | head -c "$length")


curl --silent --location --request GET "https://platform.globaltravelrule.com/api/verify/submit" \
--cert-type P12 --cert ./certificate.p12:'[MY_PASSWORD_OF_CERT]' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer [YOUR LOGIN TOKEN]" \
--header "Connection: keep-alive"
--data-raw "{
\"requestId\": \"VASP-A-verify-$random_request_id\",
\"ticker\": \"BTC\",
\"address\": \"488P2TvBRnn2WFdtGQgTZUB8DuPbeKafCf\",
\"network\": \"TESTNET\",
\"tag\": \"-\",
\"verifyType\": 2,
\"beneficiaryVasp\": \"vasp_b\",
\"encryptedPayload\": \"[PLEASAE SEE BELOW]\",
\"emptyPiiSchema\": \"[PLEASE SEE BELOW]\",
\"beneficiaryPublicKey\": \"[PLEASE SEE BELOW]\",
\"originatorPublicKey\": \"[PLEASE SEE BELOW]\",
\"fiatPrice\": \"10.02\",
\"fiatName\": \"USD\",
\"amount\": \"100\",
\"lawThreshold\": false,
}"

Here's an explanation of the payload fields.

Now we focus on how to generate encryptedPayload. First, prepare USER A's info as a string format following IVMS-101, and it looks like:

{
"ivms101": {
// ...
"identificationNumber": "ABC1234567"
// ...
}
}

Then, to generate the originatorEncryptedInfo, you could follow this code to understand:

String originatorEncryptedInfo = Curve25519Utils.encrypt(
"{\"ivms101:\":{\"identificationNumber\":\"ABC1234567\"}}",
"your_private_key",
"remote_vasp_b_public_key"
);

You will then get the encrypted info raw string, and the emptyPiiSchema will be the empty value structure of your PII information like:

{\"ivms101\":{\"identificationNumber\":\"\"}}

Additionally,

More about Empty Pii Schema

  • We strongly suggest you provide your raw info schema without value, only sending the empty structure or object, like:
JSONObject.toJSONString(new Vasp_A_PII_JP_VERSION());
// where Vasp_A_PII_JP_VERSION has properties:
{
"ivms101": {
// ...
"address": "",
"legalPerson": {},
"identificationNumber": ""
// ...
}
}

As you can see, the PII Info Structure is named: Vasp_A_PII_JP_VERSION. Depending on compliance requirements, varying amounts of information will be disclosed. You could find the registrationAuthorityCountry at the list of all VASPs or get details. (We will not mention details here about the compliance disclosure required by the country)

Now back to the topic, please put the string format to originator_pii_schema field in request data payload:

{
// ...
"encryptedPayload": "xmp0D0AJ3RD0da0d23naf03nafvsd9h3nf9q8fgb...",
"emptyPiiSchema": "{\"identificationNumber\":""}"
// ...
}

We're currently discussing how to submit the before on chain verification. Now just send the request to complete the transaction, wait for response, and if you encounter any error codes, please refer to API - Before On Chain Error Code.

After successfully sending the request, you will get a response

{
"data": {
"apiUsageInfo": {
"dailyLimit": 1000,
"inputDailyCount": 0,
"inputDailyFailedCount": 0,
"inputMonthlyCount": 1,
"inputMonthlyFailedCount": 0,
"inputTrialCount": null,
"inputTrialFailedCount": null,
"monthlyLimit": 1000,
"outputDailyCount": 3,
"outputDailyFailedCount": 0,
"outputMonthlyCount": 18,
"outputMonthlyFailedCount": 0,
"outputTrialCount": null,
"outputTrialFailedCount": null,
"trialLimit": null,
"versionType": 2
},
"requestId": "testing-example-F3zJ0D1N9wAV"
},
"originatorPublicKey": "",
"beneficiaryPublicKey": "",
"encryptedPayload": "-",
"verifyMessage": "success",
"verifyStatus": 100000,
"success": true
}

Receiving the response in the whole submit request means Beneficiary - VASP B have received the address verification and PII information. Now you can keep processing your transaction on the blockchain. (After the transaction goes to the blockchain, you will keep invoking the next API: Originator API 3 - Update txId after transaction complete)

As the originator, you may encounter errors during submission, which are directly responded to the request. The format may look like:

{
"data": null,
"verifyMessage": "error when GTR calling to target VASP who you want to interact (target client-user's server error), reason could be: 1. their firewall blocked the traffic, 2. their server not start yet",
"verifyStatus": "300001",
"success": false
}

Please see more about error codes: About HTTP Model - Server Submit

For more error code that may occur, please see VerifyStatusEnum.

Originator API 4 - Update txId after transaction complete

After API 2 is finished, it means the user has been verified by VASP B. This transaction will then be sent to the blockchain by VASP A. At the same time, you (VASP A) will determine the actual txId. You have to invoke the last process, which is to notify beneficiary (VASP B) the txId to bind the txId with requestId.

POST https://platform.globaltravelrule.com/api/verify/tx/submit

For further details on VASP list API, see: /api/verify/tx/submit

curl --silent --location --request GET "https://platform.globaltravelrule.com/api/verify/submit" \
--cert-type P12 --cert ./certificate.p12:'[MY_PASSWORD_OF_CERT]' \
--header 'Content-Type: application/json' \
--header "Authorization: Bearer [YOUR LOGIN TOKEN]" \
--header "Connection: keep-alive"
--data-raw "{
\"requestId\": \"testing-example-F3zJ0D1N9wAV\",
\"txId\": \"44e05bc5ed840f2bf1e58d6227db25132d5b89e99ba6804cf16b09703a68ceaf\"
}"

This operation will be sent to the Beneficiary - please see Step 3 - Updated TxId Callback (Before On Chain). The beneficiary will receive the txId.

Beneficiary API - Callback Preparation

Once VASP A initiates a verify request, you will first receive only address data, matched to the diagram index: 6, 7, 8. First, check if this address exists in your service. If it exists, you will receive the PII information from VASP A; otherwise, the verification will be canceled.

Now, we will show an example program written in Java Spring. You can choose your preferred language to implement it. The program must be a server that receives HTTP requests. The server domain and URL are updated when you log in to VASP server (see the login request field: callbackUrl), and you can update it anytime by just re-logging in.

  • callbackUrl is specific to the endpoint, not just a domain.
  • callbackUrl is a POST method.

Let's assume that the callbackUrl is https://example.com/my_callback. See the following example to understand the process:

@PostMapping("/my_callback")
public Object requests(@RequestBody CommonCallbackRequestOuter < ? > vaspCallback) {
// convert to wild map
Map callbackData = (Map) vaspCallback.getCallbackData();

// wild type converter
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(
DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

Integer callbackType = vaspCallback.getCallbackType()
switch (callbackType) {
case 6: // Callback Type: 6 (Before Chain Verify Address Callback)
return handleBeforeChainVerifyAddressCallback(
objectMapper.convertValue(
callbackData,
BeforeChainCallbackVerifyAddressRequest.class
)
)
break;

case 4: // Callback Type: 4(Standard Verify Result Callback)
return handleStandardVerifyResultCallback(
objectMapper.convertValue(
callbackData,
StandardCallbackResult.class
)
)
break;

case 7:
// Callback Type: 7 (Standard Verify Transaction Result Callback)
return handleStandardVerifyTransactionResultCallback(
objectMapper.convertValue(
callbackData,
BeforeChainTxCallbackResult.class
)
)
break;
}
}

Please see: CallbackTypeEnum to check the structure definition.

  • Object Mapper is the type converter that converts the raw map to the specific type.

Enum numbers are not ordered according to the flow in the diagram. For the process flow, the receiving order will be:

  • Callback Type: 6 (Before Chain Verify Address Callback)
    • Once VASP A initiates the verify, you will first receive an address from here. Now you need to confirm this address exists in your service, and respond to the request.
  • Callback Type: 4(Standard Verify Result Callback)
    • Once the address is accepted by you (VASP B), the GTR server will immediately send you this request with encrypted PII info, and you will save it. (This time will receive an empty txId but fulfill other info like encryptedPayload)
  • Callback Type: 7 (Standard Verify Transaction Result Callback)
    • Once the before-on-chain is complete, the originator may update the on-chained txId to the GTR server, and the updated txId will be notified back to VASP B by the same callback type. The second invoke will fill the txId in the field. You can refer to Figure 5-1. And look at process 20.

Beneficiary API 2 - verify address callback

Callback Type: 6 (Before Chain Verify Address Callback) - Verify Address

Please see: Callback Type: 6 (Before Chain Verify Address Callback) API structure definition. This struct should unmarshal the JSON payload from CommonCallbackRequestOuter > callbackData. And also see: API structure definition.

public JSON handleBeforeChainVerifyAddressCallback(
BeforeChainCallbackVerifyAddressRequest req) {

// you may save the request to your service

// first to check the address is exists in your service
if (find(req.address).size() != 0) {
// if exists then response ok
return {
"verifyMessage": "address exists",
"verifyStatus": 100000
}
}

// if not exists
return {
"verifyMessage": "address not exists",
"verifyStatus": 200001
}
}

If you return an HTTP status not equal to 200, the transaction will be canceled instead due to a networking and server issue. If the address does not exist, please still return HTTP 200 with an error message in data. You can still find more details in the request structure. See the Callback Type: 6 (Before Chain Verify Address Callback) API Structure.

For verifyStatusCode you could see here.

Beneficiary API 3 - receive PII callback

Callback Type: 4(Before OnChain PII Verify Callback) - Receive PII

Please see: Callback Type: 4(Before OnChain PII Verify Callback) API structure definition. This struct should unmarshal the JSON payload from CommonCallbackRequestOuter > callbackData.

public JSON handleStandardVerifyResultCallback(
StandardCallbackResult req) {
// The verify stauts code 100000 is OK
// Please see: VerifyStatusEnum
if (req.verifyStatus == 100000) {
String decrypt = Curve25519.decrypt(
result.getEncryptedOriginatorInfo(),
your_privateKey,
originator_public_key
);

// decrypted message is follow IVMS-101
// you can retrive the message what you need

// add the beneficiaryVASP info into ivms101 payload
JSONObject ivmsPayload = ...;
ivmsPayload.getObject("ivms101").setObject("BeneficiaryVASP", ...);

String encryptedPayload = Curve25519.encrypt(
ivmsPayload.toString(),
your_private_key,
originator_public_key
);

// just return the HTTP 200 means complete the travelrule (TR)
return {
"originatorPublicKey": originator_public_key,
"beneficiaryPublicKey": your_public_key,
"encryptedPayload": encryptedPayload,
"verifyMessage": "success",
"verifyStatus": 100000
}
}

// just return the HTTP 200 means complete the travelrule (TR)
// if verify status is failed still can respond as success but no need to decrypt
return {
"verifyMessage": "success",
"verifyStatus": 100000
}
}

When verifyStatus is 100000, it means the status is OK, and you will start to decrypt the PII info and retrive the data. The decrypted payload is JSON that follows the IVMS-101 specification. Detail please refer to section IVMS-101.

For the response, we only need to respond back to the GTR server to confirm that you have already received the request. The HTTP status code should be 200; otherwise, it will cancel the entire transaction as a network or server-level error, not a business-level error.

privateKey is your Curve25519 private key, if you have updated the public key at login, the encryptedOriginatorInfo using your older public key will cause a decryption error, you can decide to handle it or not. Regardless, StandardCallbackResult provides a publicKey field that you could use to map to your corresponding privateKey (see Authentication - Private key) with the legacy version.

Beneficiary API 4 - receive txId

Callback Type: 7 (Updated TxId Callback) - Receive txId

Please See: Callback Type: 7 - Updated TxId Callback.


After you complete the result, GTR will respond to a synchronized request to the originator (VASP A). After this data has completed on-chain, the `txId` will be sent back to GTR, and GTR will notify the beneficiary (VASP B).

```java
public Object handleStandardVerifyTransactionResultCallback(
BeforeChainTxCallbackResult req) {

// update tx id mappnig by request id
updateById(req.requestId, req.txId);

// just return the HTTP 200 means complete the travelrule (TR)
return {
"verifyMessage": "success",
"verifyStatus": 100000
}
}
Copyright (C) 2024 Global Travel Rule. All Rights Reserved
General
Developer