Customer Onboarding using Coinme KYC
Customer onboarding using Coinme's Customer Onboarding process requires phone 2FA and KYC with the following flow:
- Performing 2FA
- 2FA Mobile Auth
- 2FA Instant Link (if Mobile Auth is unsuccessful)
- Pre-Fill
- Verify
- 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:
- Initiate the authentication
- Follow an authentication URL
- 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
smsRedirectUrlthat will be used in the SMS sent to the user - There is a predefined
finalTargetUrlwhere 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
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"
}
}
2. Pre-Fill
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 Code | Error Message | Retriable? |
|---|---|---|
| 206-400-100-001 | Unique customer identifiers match an account that already exists for this partner | N |
| 206-400-100-011 | Customer does not meet the age requirements | N |
| 206-400-100-012 | Customer does not reside in a valid jurisdiction | N |
| 206-400-100-014 | Customer has one or more identifiers on a Coinme prohibited list | N |
| 206-400-100-020 | Customer has failed our identity verification check and may be passing incorrect data | Y |
| 206-400-100-031 | Customer has 100% name match with a sanctioned individual | N |
| 206-400-100-032 | Customer has matching hits on DOB with a sanctioned individual(s) | N |
| 206-400-100-033 | Customer has matching hits on both DOB and country with a sanctioned individual(s) | N |
| 206-400-100-034 | Customer has a person-name match score above 70%, but the sanctioned individual does not have a birthdate | N |
| 206-400-100-035 | Customer has a person-name match score above 70% and a country match, but the sanctioned individual does not have a birthdate | N |
| 206-400-100-036 | Customer's social security number or taxpayer identification number has a sanctions match | N |
| 206-500-100-999 | Unexpected internal error | Y |
| 206-400-100-999 | Invalid request parameters | Y |
Identity Reason Codes
| Reason Code | Description | ||||
|---|---|---|---|---|---|
| C2 | 2 identities that have OS or OV reason codes | ||||
| C3 | 3 identities that have OS or OV reason codes | ||||
| C4 | 4 identities that have OS or OV reason codes | ||||
| C5 | 5 or more identities that have OS or OV reason codes | ||||
| CA | Common addresses appearing across multiple seemingly unrelated identities associated with the phone number. This may indicate an increased risk of fraudulent activity. | ||||
| CF | The address matches the address of a U.S. correctional facility. | ||||
| DV | High device change velocity associated with the phone. | ||||
| HV | High velocity of change events associated with the phone. | ||||
| LP | IED < 90 days \ | \ | IMEI Change < 90 days \ | \ | MSISDN change < 90 days |
| LS | IED < 90 days \ | \ | IMSI Change < 90 days | ||
| LT | IED < 90 days old | ||||
| MM | The returned identity has a different name than the input name. | ||||
| ND | Network Status information was not available. | ||||
| NM | The line type was not mobile. | ||||
| NP | The line was classified as non-personal. | ||||
| OD | The ownership of the phone number was found before a disconnect date. | ||||
| OL | Ownership tenure is greater than 45 days. | ||||
| OO | Input 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. | ||||
| OU | This 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. | ||||
| OV | Ownership tenure of less than seven days. | ||||
| PT | The 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. | ||||
| R1 | The number of identities associated with the phone number exceeded the suggested limit. This could be an indication of a higher risk of fraudulent activity. | ||||
| RL | The phone number was associated with a high-risk line type (Non-Fixed VoIP or Prepaid). | ||||
| UC | This indicates there was insufficient data to calculate the Trust Score. |
Verify Reason Codes
| Reason Code | Description |
|---|---|
| AC | The normalized address was used to complete empty address fields before the match. |
| AU | The address was classified as undeliverable. |
| BA | The address was a business address. |
| BL | The number is associated with a business line. |
| C2 | 2 identities that have OS or OV reason codes |
| C3 | 3 identities that have OS or OV reason codes |
| C4 | 4 identities that have OS or OV reason codes |
| C5 | 5 or more identities that have OS or OV reason codes |
| CA | Common addresses appearing across multiple seemingly unrelated identities associated with the phone number. This may indicate an increased risk of fraudulent activity. |
| CF | The address matches the address of a U.S. correctional facility. |
| CN | The first and last names were combined in one field. |
| DA | The address was found to have a dual address (Ex: 123 Main St PO Box 99). |
| DI | The data returned a death indicator. |
| DT | The data retrieval timed out. |
| FN | Family name found and used in matching. |
| HR | This stands for high-rise; the address contains apartment or building sub-units. |
| IA | This 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. |
| LA | This address was classified as a low-tenure address. |
| MA | The address in the request was associated with multiple active addresses. |
| MI | The address was classified as a military address. |
| NA | The address was valid and normalized before calculating the match score. |
| NC | Name and address information was not available. |
| ND | Network Status information was not available. |
| NM | The line type was not mobile. |
| NN | Nickname found and used in matching. For example, Bill matches with William. |
| NO | The 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. |
| NP | The line was classified as non-personal. |
| NS | The first and last names were swapped. |
| NU | The phone number was updated. |
| OD | The ownership of the phone number was found before a disconnect date. |
| OL | Ownership tenure is greater than 45 days. |
| OO | Input 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. |
| OS | This indicates short ownership tenure, which is eight days–45 days. |
| OU | This indicates ownership tenure was unknown, meaning the date attributes associated with the phone number were unavailable. |
| OV | Ownership tenure is less than seven days. |
| P3 | The postal code submitted matched the first three digits. |
| P5 | The postal code submitted matched the first five digits. |
| P6 | The postal code submitted matched the first six characters. Applicable to Canadian phone numbers only. |
| P9 | The postal code submitted matched the first nine digits. |
| PM | The address was associated with a private mailbox operator (Ex: UPS Store). |
| PN | This indicates the phone number was not active. |
| PO | The address was classified as a PO Box. |
| PT | The 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. |
| PV | A successful person search verification was run. |
| R1 | The number of identities associated with the phone number exceeded the suggested limit. This could be an indication of a higher risk of fraudulent activity. |
| RA | The raw address matched better than the normalized address. |
| RL | The phone number was associated with a high-risk line type (Non-Fixed VoIP or Prepaid). |
| RM | Matching used only raw data. |
| S1 | Synthetic identity reason code one – the identity matched has multiple unique SSNs. This can indicate a higher risk of the identity being synthetic. |
| S2 | Synthetic identity reason code two – the identity matched has multiple DOB records. This can indicate a higher risk of the identity being synthetic. |
| S3 | Synthetic 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. |
| S4 | Synthetic 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. |
| UV | This indicates the address could not be verified. |
| VA | The address was vacant (unoccupied in the past 90 days). |
| XD | No driver’s license data was available to match the submitted driver’s license parameters. |
Updated 6 days ago