Customer Onboarding using Coinme KYC

Customer onboarding using Coinme's Customer Onboarding process requires phone 2FA and KYC with the following flow:

  1. Performing 2FA
    1. 2FA Mobile Auth
    2. 2FA Instant Link (if Mobile Auth is unsuccessful)
  2. Pre-Fill
  3. Verify
  4. Customer Onboarding

This KYC process is data-driven, meaning users don't need to upload a government-issued ID. Users must submit their phone number and date of birth or SSN.

🚧

Before starting, Partners need to send a list of phone numbers they intend to test with to their designated Coinme Account Manager. These numbers will then be whitelisted by the Coinme team. Once whitelisted, the Coinme team will inform the Partner and provide them with test customer data for the staging environment.

1. Performing 2FA

Successful 2FA will result in an authenticated phone number, ensuring that the user is in possession of the phone and not just a bad actor who knows someone else’s phone number. 2FA can be done in two ways: Mobile Auth and Instant Link. Both ways are similar and can be broken down into three steps:

  1. Initiate the authentication
  2. Follow an authentication URL
  3. Check authentication result

1.1 2FA - Mobile Auth

This passive flow doesn’t require user input or action, only the device's IP address.

Assumptions:

  • This authentication flow will only work if the user is connected to a mobile network. It will not work if the user is connected to WIFI
  • There is a predefined finalTargetUrl where the flow will be redirected to, after following the authentication URL

Sample API Request: POST

URL: https://caas-staging.coinme.com/services/mobileAuth
headers:
"Authorization":"asdfghj", //authorization token, required
'User-Agent: partnerapi' //required
Request body:
{
  "deviceIp": "166.137.217.20", // required; IPv4 or IPv6
  "consentTransactionId": "EWSrelease-01092020-testTMO5", // required; Unique id of the consent collected by the client
  "consentDescription": "Test description", // required; The client describes the type of consent (electronic/paper), use case and reference to the version of Terms and Conditions (T&Cs), if applicable
  "consentCollectedTimestamp": "2023-08-18" // required; Date/Time field when the original consent was collected by the client (format: YYYY-MM-DD)
}

Sample API Response:

Successful Response:
{
  "data": 
  {
    "redirectTargetUrl": "http://www.example.com?vfp=abc123..."
  },
  "errorResponse": null
}

Failure Response:
{
  "data": null,
  "errorResponse": {
    "httpStatus":"200",
    "timestamp":"2022-02-01T18:59:28.297Z",//UTC
    "path":"POST /services/mobileAuth",
    "errorData": [
      {
        "errorCode":"123-123-123-123",//The error code is just a sample showing the structure of the error code.
        "message":"Required field missing or blank"
      }
    ],
    "retry":"N"
  }
}

1.1.2 Mobile Auth - Follow the authentication URL

Upon receiving the redirectTargetUrl from the initiate call, the caller should perform a GET request on it and on completion the user will be redirected to a predefined finalTargetUrl.

Sample API Request: GET

URL: http://www.example.com?vfp=abc123.. // this is the redirectTargetUrl from the previous call

Sample API Response:

The caller will be redirected to a predefined finalTargetUrl with a vfp query parameter appended to it, for example:

URL: http://www.final-target-url.com?vfp=def456...

Extract the vfp value from the URL and use it to ask for the authentication result. Not the same as the vfp from the redirectTargetUrl.

Sample API Request: POST

URL: https://caas-staging.coinme.com/services/mobileAuthFinish
headers:
"Authorization":"asdfghj", //authorization token, required
'User-Agent: partnerapi' //required
Request body:
{
  "verificationFingerprint": "def456..." // extracted from the URL at finalTargetUrl
}

Sample API Response:

Successful Response:
{
  "data": 
  {
    "authenticatedPhoneNumber": "15553031212"
  },
  "errorResponse": null
}

Failure Response:
{
  "data": null,
  "errorResponse": {
    "httpStatus":"200",
    "timestamp":"2022-02-01T18:59:28.297Z",//UTC
    "path":"POST /services/mobileAuthFinish",
    "errorData": [
      {
        "errorCode":"123-123-123-123",//The error code is just a sample showing the structure of the error code.
        "message":"Required field missing or blank"
      }
    ],
    "retry":"N"
  }
}

1.2 2FA - Instant Link

This flow requires the user to enter their phone number and to click on the authentication link in the SMS that will be sent to that phone number.

Assumptions:

  • There is a predefined smsRedirectUrl that will be used in the SMS sent to the user
  • There is a predefined finalTargetUrl where the flow will be redirected to after following the authentication URL

Sample API Request: POST

URL: https://caas-staging.coinme.com/services/sendAuthSms
headers:
"Authorization":"asdfghj", //authorization token, required
'User-Agent: partnerapi' //required
Request body:
{
  "mobileNumber": "15553031212", // required
  "deviceIp": "166.137.217.20", // IPv4 or IPv6
}

Sample API Response:

Successful Response:
{
  "data": 
  {
    "authUrl": "www.example.com/auth/aBc123" // smsRedirectUrl with authId appended to the path (aBc123)
  },
  "errorResponse": null
}

Failure Response:
{
  "data": null,
  "errorResponse": {
    "httpStatus":"200",
    "timestamp":"2022-02-01T18:59:28.297Z",//UTC
    "path":"POST /services/sendAuthSms",
    "errorData": [
      {
        "errorCode":"123-123-123-123",//The error code is just a sample showing the structure of the error code.
        "message":"Failed to send auth SMS to the user"
      }
    ],
    "retry":"N"
  }
}

Additionally, an SMS with the smsRedirectUrl + authId is sent to the user.

1.2.2 Instant Link - Follow the authentication URL

There are two options here, depending on the choice of smsRedirectUrl.

HttpMethod: GET

First option:

The smsRedirectUrl is a URL in the Coinme domain, eg:

https://caas-staging.coinme.com/services/smsAuth/{authId}

This URL will be presented to the user in the authentication SMS. A single SMS has a limit of 160 characters, and assuming the authId is 6 characters, this would leave about 100 characters to explain to the user what the message is and that they need to click on it to authenticate.

Second option:

The smsRedirectUrl is in Partner's domain, eg:

https://www.partner.com/smsAuth/{authId}

When the user clicks on the link in the auth SMS, they will be taken to a Partner endpoint, and the Partner would need to redirect to the Coinme URL from the first option, passing the authId

Whichever option is picked, the user will ultimately be redirected to the predefined finalTargetUrl with the vfp value appended as a query parameter.

Extract the vfp value from the URL and use it to ask for the result of the authentication.

Sample API Request: POST

URL: https://caas-staging.coinme.com/services/authLinkResult
headers:
"Authorization":"asdfghj", //authorization token, required
'User-Agent: partnerapi' //required
Request body:
{
  "verificationFingerprint": "def456..." // extracted from the URL at finalTargetUrl
}

Sample API Response:

Successful Response:
{
  "data": 
  {
    "authenticatedPhoneNumber": "15553031212"
  },
  "errorResponse": null
}

Failure Response:
{
  "data": null,
  "errorResponse": {
    "httpStatus":"200",
    "timestamp":"2022-02-01T18:59:28.297Z",//UTC
    "path":"POST /services/sendAuthSms",
    "errorData": [
      {
        "errorCode":"123-123-123-123",//The error code is just a sample showing the structure of the error code.
        "message":"Required field missing or blank"
      }
    ],
    "retry":"N"
  }
}

The following API will attempt to retrieve a user's personal information, given their phone number and social security number. To keep the information confidential, Coinme uses a third party, VeryGoodSecurity, as a reverse proxy to create a token for the SSN upon request and then decrypt the token on the request to Prove’s pre-fill service.

A list of reason codes for this endpoint can be found here

The following is the API used to accomplish this:

Sample API Request: POST

URL: https://caas-staging.coinme.com/services/identity
headers:
"Authorization":"asdfghj", //authorization token, required
'User-Agent: partnerapi' //required
Request body:
{
  "ssn": "123456789", //token from VGS api call
  "phoneNumber":"1234567890" //10-digit phone number
}

Sample API Response:

Success:
{
	"data": {
		"requestId": "<<UUID>>",
		"status": 0,
		"description": "Success.",
		"additionalInfo": null,
		"response": {
			"transactionId": "12228113388",
			"phoneNumber": "<<User Phone Number>>",
			"lineType": "Landline",
			"carrier": "<<Carrier>>",
			"countryCode": "US",
			"reasonCodes": [
			<<List of 2-digit reason codes>>
			],
			"individual": {
				"firstName": "<<fname>>",
				"lastName": "<<lname>>",
				"addresses": [
					{
						"address": "<<street>>",
						"extendedAddress": "",
						"city": "<<city>>",
						"region": "<<state>>",
						"postalCode": "<<zip>>"
					}
				],
				"emailAddresses": null,
				"dob": "YYYY-MM-dd",
				"ssn": "<<SSN Token>>"
			}
		}
	},
	"errorResponse": null
}

Errors:
Invalid phone number:

{
	"data": null,
	"errorResponse": {
		"timestamp": "2023-08-30T04:43:56.286644803Z",
		"httpStatus": 200,
		"errorData": [
			{
				"errorCode": "131-400-110-004",
				"message": "Subscriber is not found on the required whitelist. - "
			}
		],
		"path": "POST /prove/identity"
	}
}

Invalid phone number length

{
	"data": null,
	"errorResponse": {
		"timestamp": "2023-08-30T04:45:35.271987363Z",
		"httpStatus": 200,
		"errorData": [
			{
				"errorCode": "131-400-110-003",
				"message": "Parameter is invalid. - phoneNumber invalid."
			}
		],
		"path": "POST /prove/identity"
	}
}

Missing phone number:

{
	"data": null,
	"errorResponse": {
		"timestamp": "2023-08-30T04:46:36.775723042Z",
		"httpStatus": 200,
		"errorData": [
			{
				"errorCode": "131-400-110-667",
				"message": "phoneNumber must not be blank"
			}
		],
		"path": "POST /prove/identity"
	}
}

Missing SSN:

{
	"data": null,
	"errorResponse": {
		"timestamp": "2023-08-30T04:45:53.539677598Z",
		"httpStatus": 200,
		"errorData": [
			{
				"errorCode": "131-400-110-667",
				"message": "ssn must not be blank"
			}
		],
		"path": "POST /prove/identity"
	}
}

Invalid SSN Format or No response found:

{
	"data": null,
	"errorResponse": {
		"timestamp": "2023-08-30T04:47:18.453023706Z",
		"httpStatus": 200,
		"errorData": [
			{
				"errorCode": "131-400-110-006",
				"message": "prove could not find the requested user: full ssn mismatch"
			}
		],
		"path": "POST /prove/identity"
	}
}

This endpoint must be called to verify that the submitted data correctly matches the customer associated with it. The more data input, the more accurate a result can be returned. To keep the information confidential, Coinme uses a third party, VeryGoodSecurity, as a reverse proxy to create a token for the ssn upon the request and then decrypt the token on the request to Prove’s pre-fill service.

A list of reason codes for this endpoint can be found here

Request

Sample API Request: POST

URL: https://caas-staging.coinme.com/services/verify
headers:
"Authorization":"asdfghj", //authorization token, required
'User-Agent: partnerapi' //required
Request body:
{
  "ssn": "123456789" //token from VGS api call -- Mandatory
  "phoneNumber":"1234567890" //10-digit phone number -- Mandatory
  "consentStatus": "optedIn" //enum of optedIn, optedOut, notCollected, unknown --Optional
  "firstName": "fname" //Optional
  "lastName": "lname" //Optional
  "address": "123 Street St." //Optional
  "extendedAddress":"Apt. 2" //Optional
  "city":"Seattle" //Optional
  "region":"WA" //State -- Optional
  "postalCode":"12345" //Optional
  "dob": "YYYY-MM-dd" //Optional
}

Response

Sample API Response:

Success:
{
	"data": {
		"requestId": "<<UUID>>",
		"status": 0,
		"description": "Success.",
		"additionalInfo": null,
		"response": {
			"verified": true, //boolean to say if the data matches
			"transactionId": "12228136833",
			"payfoneAlias": "<<payfoneAliasId>>",
			"phoneNumber": "<<inputPhoneNumber>>",
			"lineType": "Landline",
			"carrier": "<<carrier>>",
			"countryCode": "US",
			"reasonCodes": [
			<<List of 2-digit reason codes>>
			],
			"address": {
				"distance": 0.0,
				"addressScore": 100, //0-100 on matching of address
				"streetNumber": 100, //0-100 on matching of street number
				"street": false, // boolean on street match
				"city": true, //boolean on city match
				"region": true, //boolean on region match
				"postalCode": true //boolean on postal code match
			},
			"name": {
				"nameScore": 100, //0-100 on matching of full name
				"firstName": 100, //0-100 on matching of first name
				"lastName": 100 //0-100 on matching of last name
			},
			"identifiers": {
				"last4": false, //if last4 used instead of full ssn (should always be false in this case)
				"dob": true, //boolean on dob matches
				"driversLicenseState": false, //boolean on drivers license state match (not supported at this time)
				"driversLicenseNumber": false, //boolean on drivers license number match (not supported at this time)
				"ssn": true //boolean on ssn match
			},
			"knowYourCustomer": { //AML or Sanctions hits
				"totalHits": 1, 
				"amlTypeLists": [
					{
						"amlType": "pep-class-1",
						"listHits": 1,
						"fields": [
							{
								"source": "europe-sanction-list",
								"name": "TestName",
								"value": "TestValue"
							}
						]
					}
				]
			},
			"matchDataType": null //not suported at this time
		}
	},
	"errorResponse": null
}

Errors:
Invalid phone number:

{
	"data": null,
	"errorResponse": {
		"timestamp": "2023-08-30T04:43:56.286644803Z",
		"httpStatus": 200,
		"errorData": [
			{
				"errorCode": "131-400-110-004",
				"message": "Subscriber is not found on the required whitelist. - "
			}
		],
		"path": "POST /prove/identity"
	}
}

Invalid phone number length

{
	"data": null,
	"errorResponse": {
		"timestamp": "2023-08-30T04:45:35.271987363Z",
		"httpStatus": 200,
		"errorData": [
			{
				"errorCode": "131-400-110-003",
				"message": "Parameter is invalid. - phoneNumber invalid."
			}
		],
		"path": "POST /prove/identity"
	}
}

Missing phone number:

{
	"data": null,
	"errorResponse": {
		"timestamp": "2023-08-30T04:46:36.775723042Z",
		"httpStatus": 200,
		"errorData": [
			{
				"errorCode": "131-400-110-667",
				"message": "phoneNumber must not be blank"
			}
		],
		"path": "POST /prove/identity"
	}
}

Missing SSN:

{
	"data": null,
	"errorResponse": {
		"timestamp": "2023-08-30T04:45:53.539677598Z",
		"httpStatus": 200,
		"errorData": [
			{
				"errorCode": "131-400-110-667",
				"message": "ssn must not be blank"
			}
		],
		"path": "POST /prove/identity"
	}
}

Verify Customer KYC and create a new customer account if one does not exist

For staging, the Customer Onboarding URL ishttps://secure.apis.coinme.org/services/customeronboarding For production, ishttps://secure.apis.coinme.com/services/customeronboarding

Assumptions:

  • The Partner should perform 2FA and ensure it is passed. They should not send onboarding requests if 2FA is not passed.

A list of reason codes for this endpoint can be found here

Sample API Request: POST

URL: https://secure.apis.coinme.org/services/customeronboarding
headers:
"Authorization":"asdfghj", //authorization token, required
'User-Agent: partnerapi' //required
Request body:
{
    "email": "[email protected]",//required
    "fullName": {
        "firstName": "TestFirst",//required
        "middleName": "TestMiddle",
        "lastName": "TestLast",//required
        "otherNames":[
          "secondLastName",
          "otherName1",
          "otherName2"
        ]
    },
    "address": {
        "line1": "1234 Example St",//required
        "line2": "Apt 4",
        "city": "Foo City",//required
        "state": "CA",//required
        "postalCode": "93420",//required
        "country": "USA"//required, 3 Character abbreviation
    },
    "phoneNumber": {
        "countryCode": "1",//required
        "number": "8001234567"//required
    },
    "dateOfBirth": "1965-02-10",//required
    "SSN": "123-123-1234"//required
}

Sample API Response:

Successful Response:
{
  "data": 
  {
    "customerId": "654987321"
  },
  "errorResponse": null
}

Failure Response:
{
  "data": null,
  "errorResponse": {
    "httpStatus":"200",
    "timestamp":"2022-02-01T18:59:28.297Z",//UTC
    "path":"/services/customeronboarding POST",
    "errorData": [
      {
        "errorCode":"123-123-123-123",//The error code is just a sample showing the structure of the error code.
        "message":"Customer identity could not be validated"
      }
    ],
    "retry":"N"
  }
}

Once a successful response is received from Customer Onboarding, then a customer account is onboarded with the associated customerId.


POST /customeronboarding

Error messages for errors that occur while performing KYC:

Error Codes

Error CodeError MessageRetriable?
206-400-100-001Unique customer identifiers match an account that already exists for this partnerN
206-400-100-011Customer does not meet the age requirementsN
206-400-100-012Customer does not reside in a valid jurisdictionN
206-400-100-014Customer has one or more identifiers on a Coinme prohibited listN
206-400-100-020Customer has failed our identity verification check and may be passing incorrect dataY
206-400-100-031Customer has 100% name match with a sanctioned individualN
206-400-100-032Customer has matching hits on DOB with a sanctioned individual(s)N
206-400-100-033Customer has matching hits on both DOB and country with a sanctioned individual(s)N
206-400-100-034Customer has a person-name match score above 70%, but the sanctioned individual does not have a birthdateN
206-400-100-035Customer has a person-name match score above 70% and a country match, but the sanctioned individual does not have a birthdateN
206-400-100-036Customer's social security number or taxpayer identification number has a sanctions matchN
206-500-100-999Unexpected internal errorY
206-400-100-999Invalid request parametersY

Identity Reason Codes

Reason CodeDescription
C22 identities that have OS or OV reason codes
C33 identities that have OS or OV reason codes
C44 identities that have OS or OV reason codes
C55 or more identities that have OS or OV reason codes
CACommon addresses appearing across multiple seemingly unrelated identities associated with the phone number. This may indicate an increased risk of fraudulent activity.
CFThe address matches the address of a U.S. correctional facility.
DVHigh device change velocity associated with the phone.
HVHigh velocity of change events associated with the phone.
LPIED < 90 days \\IMEI Change < 90 days \\MSISDN change < 90 days
LSIED < 90 days \\IMSI Change < 90 days
LTIED < 90 days old
MMThe returned identity has a different name than the input name.
NDNetwork Status information was not available.
NMThe line type was not mobile.
NPThe line was classified as non-personal.
ODThe ownership of the phone number was found before a disconnect date.
OLOwnership tenure is greater than 45 days.
OOInput identity was verified (verified=true). The connection of the ownership or association to this phone number was not recent (internal note: greater than five years). However, no known newer ownership or association was found, a.k.a. the ownership is “older”. See also code NO.
OUThis indicates ownership tenure was unknown, meaning the date attributes associated with the phone number were unavailable. Ownership tenure is how long the identity has been associated with a phone number, based on when the ownership was first seen. See also codes OL, OS, and OV.
OVOwnership tenure of less than seven days.
PTThe phone number was in a ported state. Any number not on its original home carrier will have a PT reason code. This is NOT indicative of a recent carrier port.
R1The number of identities associated with the phone number exceeded the suggested limit. This could be an indication of a higher risk of fraudulent activity.
RLThe phone number was associated with a high-risk line type (Non-Fixed VoIP or Prepaid).
UCThis indicates there was insufficient data to calculate the Trust Score.

Verify Reason Codes

Reason CodeDescription
ACThe normalized address was used to complete empty address fields before the match.
AUThe address was classified as undeliverable.
BAThe address was a business address.
BLThe number is associated with a business line.
C22 identities that have OS or OV reason codes
C33 identities that have OS or OV reason codes
C44 identities that have OS or OV reason codes
C55 or more identities that have OS or OV reason codes
CACommon addresses appearing across multiple seemingly unrelated identities associated with the phone number. This may indicate an increased risk of fraudulent activity.
CFThe address matches the address of a U.S. correctional facility.
CNThe first and last names were combined in one field.
DAThe address was found to have a dual address (Ex: 123 Main St PO Box 99).
DIThe data returned a death indicator.
DTThe data retrieval timed out.
FNFamily name found and used in matching.
HRThis stands for high-rise; the address contains apartment or building sub-units.
IAThis indicates an inactive address, such as new developments having addresses but being inactive until somebody moves in. Or, after Hurricane Katrina, addresses in the affected area were marked as inactive for a time.
LAThis address was classified as a low-tenure address.
MAThe address in the request was associated with multiple active addresses.
MIThe address was classified as a military address.
NAThe address was valid and normalized before calculating the match score.
NCName and address information was not available.
NDNetwork Status information was not available.
NMThe line type was not mobile.
NNNickname found and used in matching. For example, Bill matches with William.
NOThe input identity was verified (verified=true). Newer ownership (identity) was recently associated with the phone number used in establishing the verification. See also code OO.
NPThe line was classified as non-personal.
NSThe first and last names were swapped.
NUThe phone number was updated.
ODThe ownership of the phone number was found before a disconnect date.
OLOwnership tenure is greater than 45 days.
OOInput identity was verified (verified=true). The connection of the ownership or association to this phone number was not recent (internal note: greater than five years). However, no known newer ownership or association was found, a.k.a. the ownership is “older”. See also code NO.
OSThis indicates short ownership tenure, which is eight days–45 days.
OUThis indicates ownership tenure was unknown, meaning the date attributes associated with the phone number were unavailable.
OVOwnership tenure is less than seven days.
P3The postal code submitted matched the first three digits.
P5The postal code submitted matched the first five digits.
P6The postal code submitted matched the first six characters. Applicable to Canadian phone numbers only.
P9The postal code submitted matched the first nine digits.
PMThe address was associated with a private mailbox operator (Ex: UPS Store).
PNThis indicates the phone number was not active.
POThe address was classified as a PO Box.
PTThe phone number was in a ported state. Any number not on its original home carrier will have a PT reason code. This is NOT indicative of a recent carrier port.
PVA successful person search verification was run.
R1The number of identities associated with the phone number exceeded the suggested limit. This could be an indication of a higher risk of fraudulent activity.
RAThe raw address matched better than the normalized address.
RLThe phone number was associated with a high-risk line type (Non-Fixed VoIP or Prepaid).
RMMatching used only raw data.
S1Synthetic identity reason code one – the identity matched has multiple unique SSNs. This can indicate a higher risk of the identity being synthetic.
S2Synthetic identity reason code two – the identity matched has multiple DOB records. This can indicate a higher risk of the identity being synthetic.
S3Synthetic identity reason code three – the identity matched has a high number of relatives with the same/similar name. This can indicate a higher risk of the identity being synthetic.
S4Synthetic identity reason code four – the identity matched has an SSN issued before either the submitted DOB or—if no DOB was submitted—the identity’s DOB. This can indicate a higher risk of the identity being synthetic.
UVThis indicates the address could not be verified.
VAThe address was vacant (unoccupied in the past 90 days).
XDNo driver’s license data was available to match the submitted driver’s license parameters.

What’s Next