SEP: 0012
Title: KYC API
Author: Interstellar
Status: Active
Created: 2018-09-11
Updated: 2021-08-17
Version 1.8.0
This SEP defines a standard way for stellar clients to upload KYC (or other) information to anchors and other services. SEP-6 and SEP-31 use this protocol, but it can serve as a stand-alone service as well.
This SEP was made with these goals in mind:
- interoperability
- Allow a customer to enter their KYC information to their wallet once and use it across many services without re-entering information manually
- handle the most common 80% of use cases
- handle image and binary data
- support the set of fields defined in SEP-9
- support authentication via SEP-10
- support the provision of data for SEP-6, SEP-24, SEP-31, and others
- give customers control over their data by supporting complete data erasure
To support this protocol an anchor acts as a server and implements the specified REST API endpoints, while a wallet implements a client that consumes the API. The goal is interoperability, so a wallet implements a single client according to the protocol, and will be able to interact with any compliant anchor. Similarly, an anchor that implements the API endpoints according to the protocol will work with any compliant wallet.
- An anchor must define the location of their
KYC_SERVER
orTRANSFER_SERVER
in theirstellar.toml
. This is how a client app knows where to find the anchor's server. A client app will send KYC requests to theKYC_SERVER
if it is specified, otherwise to theTRANSFER_SERVER
. - Anchors and clients must support SEP-10 web authentication and use it for all SEP-12 endpoints.
GET /customer
: Check the status of a customers infoPUT /customer
: Idempotent upload of customer infoPUT /customer/callback
: Set a callback for a wallet to receive status updates from the anchorPUT /customer/verification
: Idempotent upload of data for verifying customer infoDELETE /customer/[account]
: Deletion of customer infoPOST /customer/files
GET /customer/files
Clients should submit the JWT previously obtained from the anchor via the SEP-10 authentication flow. The JWT should be included in all requests as request header:
Authorization: Bearer <JWT>
Client applications can use a single Stellar account to hold multiple users' funds. To distinguish users that use the same Stellar account, the decoded SEP-10 JWT's sub
value may contain a memo value after the Stellar account (G...:2810101841641761712
) OR the sub
value will be a Muxed Account (M...
). The anchor should use this sub
attribute in their data model to identify unique users.
This document will refer to these accounts as shared accounts. See the SEP-10 Memos and Muxed Accounts sections for more information.
SEP-12 implementations should expect the memo in the SEP-10 JWT's sub
field to match the memo request parameter and should return an error response if they do not. If the JWT's sub
field does not contain a muxed account or memo then the memo request parameters may contain any value. This behavior allows shared account owners, such as SEP-31 sending anchors, to submit user information about their users using the memos assigned to their users.
Note that Stellar accounts are either shared or they are not. This means anchors should ensure that a Stellar account previously authenticated with a memo should not be authenticated later without a memo. Conversely, an account that was previously authenticated without a memo should not be later authenticated as a shared account.
All endpoints accept in requests the following Content-Type
s:
multipart/form-data
application/x-www-form-urlencoded
application/json
multipart/form-data
should be used for requests including binary data type values from SEP-9. Any of the above encoding schemes may be used when requests do not include binary data.
All endpoints respond with content type:
application/json
This endpoint allows clients to:
- Fetch the fields the server requires in order to register a new customer via a
PUT /customer
request
If the server does not have a customer registered for the parameters sent in the request, it should return the fields required in the response. The same response should be returned when no parameters are sent.
- Check the status of a customer that may already be registered
This allows clients to check whether the customers information was accepted, rejected, or still needs more info. If the server still needs more info, or the server needs updated information, it should return the fields required.
GET [KYC_SERVER]/customer&type=<customer-type>
GET [KYC_SERVER]/customer?account=<stellar-account>&type=<customer-type>
GET [KYC_SERVER]/customer?account=<stellar-account>&memo=<memo>&type=<customer-type>
GET [KYC_SERVER]/customer?account=<muxed-account>&type=<customer-type>
GET [KYC_SERVER]/customer?id=<customer-id>&type=<customer-type>
Name | Type | Description |
---|---|---|
id |
string |
(optional) The ID of the customer as returned in the response of a previous PUT request. If the customer has not been registered, they do not yet have an id . |
account |
G... or M... string |
(deprecated, optional) This field should match the sub value of the decoded SEP-10 JWT. |
memo |
string | (optional) the client-generated memo that uniquely identifies the customer. If a memo is present in the decoded SEP-10 JWT's sub value, it must match this parameter value. If a muxed account is used as the JWT's sub value, memos sent in requests must match the 64-bit integer subaccount ID of the muxed account. See the Shared Accounts section for more information. |
memo_type |
string | (deprecated, optional) type of memo . One of text , id or hash . Deprecated because memos should always be of type id , although anchors should continue to support this parameter for outdated clients. If hash , memo should be base64-encoded. If a memo is present in the decoded SEP-10 JWT's sub value, this parameter can be ignored. See the Shared Accounts section for more information. |
type |
string | (optional) the type of action the customer is being KYCd for. See the Type Specification below. |
lang |
string | (optional) Defaults to en . Language code specified using ISO 639-1. Human readable descriptions, choices, and messages should be in this language. |
The client can always use the account
and optional memo
parameters to uniquely identify a customer. However, if a PUT /customer
request has already been made for the customer, the client may use the id
returned in the response instead.
Different types of customers may have different KYC requirements depending on the action a given customer wants to take. The type
parameter is used to specify the action a customer wants to make so the server can identify what information must be collected. SEP-12 does not define what type
values are accepted and instead leaves that to the other protocols, such as SEP-6 and SEP-31, that use SEP-12 for the actions associated with those protocols.
For example, if a customer is being KYC'd as a SEP-31 sender, they may only require full name and email, but a SEP-31 receiver would require banking information in order to receive a direct deposit. The type
parameter could also be used to indicate that a customer is an organization instead of a person.
Note that it is possible for the same customer to have different status
values for different type
parameters. For example, a customer could have an ACCEPTED
status for the small-transaction-amount
type
parameter but have a NEEDS_INFO
status for the large-transaction-amount
type. Therefore it is recommended to always pass the type
parameter when making requests to GET /customer
or PUT /customer
, even though the field is optional to accomodate for implementations that do not require type
.
If the implementor requires the same set of fields for all customers, there is no need for the type
parameter.
Name | Type | Description |
---|---|---|
id |
string | (optional) ID of the customer, if the customer has already been created via a PUT /customer request. |
status |
string | Status of the customers KYC process. |
fields |
object | (optional) An object containing the fields the anchor has not yet received for the given customer of the type provided in the request. Required for customers in the NEEDS_INFO status. See Fields for more detailed information. |
provided_fields |
object | (optional) An object containing the fields the anchor has received for the given customer. See Provided Fields for more detailed information. Required for customers whose information needs verification via PUT /customer/verification . |
message |
string | (optional) Human readable message describing the current state of customer's KYC process. |
// The case when a customer has been successfully KYC'd and approved
{
"id": "d1ce2f48-3ff1-495d-9240-7a50d806cfed",
"status": "ACCEPTED",
"provided_fields": {
"first_name": {
"description": "The customer's first name",
"type": "string",
"status": "ACCEPTED"
},
"last_name": {
"description": "The customer's last name",
"type": "string",
"status": "ACCEPTED"
},
"email_address": {
"description": "The customer's email address",
"type": "string",
"status": "ACCEPTED"
}
}
}
// The case when a customer has provided some but not all required information
{
"id": "d1ce2f48-3ff1-495d-9240-7a50d806cfed",
"status": "NEEDS_INFO",
"fields": {
"mobile_number": {
"description": "phone number of the customer",
"type": "string"
},
"email_address": {
"description": "email address of the customer",
"type": "string",
"optional": true
}
},
"provided_fields": {
"first_name": {
"description": "The customer's first name",
"type": "string",
"status": "ACCEPTED"
},
"last_name": {
"description": "The customer's last name",
"type": "string",
"status": "ACCEPTED"
}
}
}
// The case when an anchor requires info about an unknown customer
{
"status": "NEEDS_INFO",
"fields": {
"email_address": {
"description": "Email address of the customer",
"type": "string",
"optional": true
},
"id_type": {
"description": "Government issued ID",
"type": "string",
"choices": [
"Passport",
"Drivers License",
"State ID"
]
},
"photo_id_front": {
"description": "A clear photo of the front of the government issued ID",
"type": "binary"
}
}
}
// The case when the Anchor is processing KYC information
{
"id": "46116754-695e-43f6-84c4-8c05e50a7b12",
"status": "PROCESSING",
"message": "Photo ID requires manual review. This process typically takes 1-2 business days.",
"provided_fields": {
"photo_id_front": {
"description": "A clear photo of the front of the government issued ID",
"type": "binary",
"status": "PROCESSING"
}
}
}
// The case when a customer has been rejected and cannot be KYC'd
{
"id": "d1ce2f48-3ff1-495d-9240-7a50d806cfed",
"status": "REJECTED",
"message": "This person is on a sanctions list"
}
// the case when the anchor requires a verification code to be sent to the server
{
"id": "d1ce2f48-3ff1-495d-9240-7a50d806cfed",
"status": "NEEDS_INFO",
"provided_fields": {
"mobile_number": {
"description": "phone number of the customer",
"type": "string",
"status": "VERIFICATION_REQUIRED"
}
}
}
Status | Description |
---|---|
ACCEPTED | All required KYC fields have been accepted and the customer has been validated for the type passed. It is possible for an accepted customer to move back to another status if the KYC provider determines it needs more info at a later date, or if the customer shows up on a sanctions list. |
PROCESSING | KYC process is in flight and client can check again in the future to see if any further info is needed. |
NEEDS_INFO | More info needs to be provided to finish KYC for this customer. The fields entry is required in this case. |
REJECTED | This customer's KYC has failed and will never succeed. The message must be supplied in this case. |
The fields object defines the pieces of information the anchor has not yet received for the customer. It is required for the NEEDS_INFO
status but may be included with any status. Fields should be specified as an object with keys representing the SEP-9 field names required.
Customers in the ACCEPTED
status should not have any required fields present in the object, since all required fields should have already been provided.
Property | Type | Description |
---|---|---|
type |
enum | The data type of the field value. Can be string , binary , number , or date |
description |
string | A human-readable description of this field, especially important if this is not a SEP-9 field. |
choices |
array | (optional) An array of valid values for this field. |
optional |
boolean | (optional) A boolean whether this field is required to proceed or not. Defaults to false. |
The provided fields object defines the pieces of information the anchor has received for the customer. It is not required unless one or more of provided fields require verification via PUT /customer/verification
.
Property | Type | Description |
---|---|---|
type |
enum | The data type of the field value. Can be string , binary , number , or date |
description |
string | A human-readable description of this field, especially important if this is not a SEP-9 field. |
choices |
array | (optional) An array of valid values for this field. |
optional |
boolean | (optional) A boolean whether this field is required to proceed or not. Defaults to false. |
status |
string | (optional) One of the values described in Provided Field Statuses. If the server does not wish to expose which field(s) were accepted or rejected, this property can be omitted. |
error |
string | (optional) The human readable description of why the field is REJECTED . |
Status | Description |
---|---|
ACCEPTED | The field has been validated. When all required fields are accepted, the Customer Status should also be accepted. |
PROCESSING | The field is being validated. The client can make GET /customer requests to check on the result of this validation in the future. |
REJECTED | The field was in the PROCESSING status but did not pass validation. If the client may resubmit this field, the Customer Status should be NEEDS_INFO , otherwise it should be REJECTED . |
VERIFICATION_REQUIRED | The field must be verified using the PUT /customer/verification endpoint. For example, the mobile_number field could be placed in this status until a confirmation code is sent to the customer and passed back to the this endpoint. |
For requests containing an id
parameter value that does not exist or exists for a customer created by another anchor, return a 404
response.
{
"error": "customer not found for id: 7e285e7d-d984-412c-97bc-909d0e399fbf"
}
For invalid requests, return a 400
response describing the issue.
For example:
{
"error": "unrecognized 'type' value. see valid values in the /info response"
}
Upload customer information to an anchor in an authenticated and idempotent fashion.
PUT [KYC_SERVER || TRANSFER_SERVER]/customer
Content-Type: multipart/form-data;boundary="boundary"
--boundary
Content-Disposition: form-data; name="account"
GBORFR3GDNVZ5PLUTBDQHKGWVD26CQUHORO2T3SDQ2JPLGLUJCCA5GK6
--boundary
Content-Disposition: form-data; name="memo"
21bf91a4-7db1-401d-8108-fab7660a45d6
--boundary--
Content-Type: application/x-www-form-urlencoded
account=GBORFR3GDNVZ5PLUTBDQHKGWVD26CQUHORO2T3SDQ2JPLGLUJCCA5GK6&memo=1273187815064134326&type=sep31-sender
Content-Type: application/json
{
"account": "GBORFR3GDNVZ5PLUTBDQHKGWVD26CQUHORO2T3SDQ2JPLGLUJCCA5GK6",
"memo": "10638330804770506835",
"type": "counterparty_organization"
}
Name | Type | Description |
---|---|---|
id |
string | (optional) The id value returned from a previous call to this endpoint. If specified, no other parameter is required. |
account |
G... or M... string |
(deprecated, optional) This field should match the sub value of the decoded SEP-10 JWT. |
memo |
string | (optional) the client-generated memo that uniquely identifies the customer. If a memo is present in the decoded SEP-10 JWT's sub value, it must match this parameter value. If a muxed account is used as the JWT's sub value, memos sent in requests must match the 64-bit integer subaccount ID of the muxed account. See the Shared Accounts section for more information. |
memo_type |
string | (deprecated, optional) type of memo . One of text , id or hash . Deprecated because memos should always be of type id , although anchors should continue to support this parameter for outdated clients. If hash , memo should be base64-encoded. If a memo is present in the decoded SEP-10 JWT's sub value, this parameter can be ignored. See the Shared Accounts section for more information. |
type |
string | (optional) The type of the customer as defined in the Type Specification. |
The wallet should also transmit one or more of the fields listed in SEP-9, depending on what the anchor has indicated it needs.
When uploading data for fields specificed in SEP-9, binary
type fields (typically files) should be submitted after all other fields. The reason for this is that some web servers require binary
fields at the end so that they know when they can begin processing the request as a stream.
If the anchor received and stored the data successfully, it should respond with a 202 Accepted
or 200 Success
HTTP status code in addition to a response body containing the customer ID.
Name | Type | Description |
---|---|---|
id |
string |
An identifier for the updated or created customer |
{
"id": "391fb415-c223-4608-b2f5-dd1e91e3a986"
}
The id
can be used in future requests to retrieve the status of the customer or update the customer's information. It may also be used in other SEPs to identify the customer.
Anchors should return 404 Not Found
for requests including an id
value that does not exist in the database. Anchors should also return 404
when the id
specified in the request was initially used to create a customer for a different stellar account.
{
"error": "customer with `id` not found"
}
All error responses should contain details under the error
key.
For example:
{
"error": "'photo_id_front' cannot be decoded. Must be jpg or png."
}
This endpoint allows servers to accept data values, usually confirmation codes, that verify a previously provided field via PUT /customer
, such as mobile_number
or email_address
. Note that while fields such as photo_proof_residence
or notary_approval_of_photo_id
are verifications of other fields described in SEP-9, the server does not require the associated fields before verification can be accomplished, so this endpoint would not be useful for such fields.
Fields in the VERIFICATION_REQUIRED
status require a request to this endpoint.
PUT [KYC_SERVER || TRANSFER_SERVER]/customer/verification
Name | Type | Description |
---|---|---|
id |
string | The ID of the customer as returned in the response of a previous PUT request. |
*_verification |
string | One or more SEP-9 fields appended with _verification . |
{
"id": "391fb415-c223-4608-b2f5-dd1e91e3a986",
"mobile_number_verification": "2735021"
}
Success responses should return a 200 Success
status as well as a body matching the GET /customer response schema. The field statuses for which verifications were sent must be updated to either PROCESSING
or ACCEPTED
.
All error responses should contain details under the error
key. If the id
provide is not known, a 404
status should be returned. On any other client error, use a 400
status.
{
"id": "d1ce2f48-3ff1-495d-9240-7a50d806cfed",
"status": "ACCEPTED",
"provided_fields": {
"mobile_number": {
"description": "phone number of the customer",
"type": "string",
"status": "ACCEPTED"
}
}
}
{
"error": "The provided confirmation code was invalid."
}
Delete all personal information that the anchor has stored about a given customer. [account]
is the Stellar account ID (G...
) of the customer to delete. This request must be authenticated (via SEP-10) as coming from the owner of the account that will be deleted. If account
does not uniquely identify an individual customer (a shared account), the client should include the memo
and memo_type
fields in the request body.
DELETE [KYC_SERVER || TRANSFER_SERVER]/customer/[account]
Name | Type | Description |
---|---|---|
memo |
string | (optional) the client-generated memo that uniquely identifies the customer. If a memo is present in the decoded SEP-10 JWT's sub value, it must match this parameter value. If a muxed account is used as the JWT's sub value, memos sent in requests must match the 64-bit integer subaccount ID of the muxed account. See the Shared Accounts section for more information. |
memo_type |
string | (deprecated, optional) type of memo . One of text , id or hash . Deprecated because memos should always be of type id , although anchors should continue to support this parameter for outdated clients. If hash , memo should be base64-encoded. If a memo is present in the decoded SEP-10 JWT's sub value, this parameter can be ignored. See the Shared Accounts section for more information. |
Situation | Response |
---|---|
Success | 200 OK |
Client not authenticated properly | 401 Unauthorized |
Anchor has no information on the customer | 404 Not Found |
Allow the wallet to provide a callback URL to the anchor. The provided callback URL will replace (and supercede) any previously-set callback URL for this account.
Whenever the user's status
field changes, the anchor will issue a POST request to the callback URL. The payload of the POST request will be the same as the response of GET /customer
.
It's the wallet's responsibility to send a working callback URL to the anchor.
Anchors will submit POST requests until the user's status changes to ACCEPTED
or REJECTED
. If a wallet needs to watch a user's KYC status after that, it will need to set a callback again.
PUT [KYC_SERVER || TRANSFER_SERVER]/customer/callback
Name | Type | Description |
---|---|---|
id |
string |
(optional) The ID of the customer as returned in the response of a previous PUT request. If the customer has not been registered, they do not yet have an id . |
account |
G... or M... string |
(deprecated, optional) This field should match the sub value of the decoded SEP-10 JWT. |
memo |
string | (optional) the client-generated memo that uniquely identifies the customer. If a memo is present in the decoded SEP-10 JWT's sub value, it must match this parameter value. If a muxed account is used as the JWT's sub value, memos sent in requests must match the 64-bit integer subaccount ID of the muxed account. See the Shared Accounts section for more information. |
memo_type |
string | (deprecated, optional) type of memo . One of text , id or hash . Deprecated because memos should always be of type id , although anchors should continue to support this parameter for outdated clients. If hash , memo should be base64-encoded. If a memo is present in the decoded SEP-10 JWT's sub value, this parameter can be ignored. See the Shared Accounts section for more information. |
url |
string |
A callback URL that the SEP-12 server will POST to when the state of the account changes. |
Situation | Response |
---|---|
Success | 200 OK |
Client not authenticated properly | 401 Unauthorized |
Anchor has no information on the customer | 404 Not Found |
Passing binary fields such as photo_id_front
or organization.photo_proof_address
in PUT /customer requests must be done using the multipart/form-data
content type. This is acceptable in most cases, but multipart/form-data
does not support nested data structures such as arrays or sub-objects.
This endpoint is intended to decouple requests containing binary fields from requests containing nested data structures, supported by content types such as application/json
. This endpoint is optional and only needs to be supported if your use case requires accepting nested data structures in PUT /customer
requests.
Once a file has been uploaded using this endpoint, it's file_id
can be used in subsequent PUT /customer
requests. The field name for the file_id
should be the appropriate SEP-9 field followed by _file_id
. For example, if file_abc
is returned as a file_id
from POST /customer/files, it can be used in a PUT /customer
request like so:
{
"account":"GBORFR3GDNVZ5PLUTBDQHKGWVD26CQUHORO2T3SDQ2JPLGLUJCCA5GK6",
"memo":"21bf91a4-7db1-401d-8108-fab7660a45d6",
"memo_type":"text",
"photo_id_front_file_id": "file_abc"
}
{
"id": "2f417dab-18d2-4081-8c59-c9d3afb59d3f",
"photo_id_front_file_id": "file_abc"
}
POST [KYC_SERVER || TRANSFER_SERVER]/customer/files
Name | Type | Description |
---|---|---|
file |
binary | A file to upload. The file should follow the specifications of RFC 2388 (which defines file transfers for the multipart/form-data protocol). |
Name | Type | Description |
---|---|---|
file_id |
string | Unique identifier for the object. |
content_type |
string | The Content-Type of the file. |
size |
integer | The size in bytes of the file object. |
expires_at |
UTC ISO 8601 string | (optional) The date and time the file will be discarded by the server if not referenced by the client in a PUT /customer request. |
customer_id |
string | (optional) The id of the customer this file is associated with. If the customer record does not yet exist this will be null . |
{
"file_id": "file_d3d54529-6683-4341-9b66-4ac7d7504238",
"content_type": "image/jpeg",
"size": 4089371,
"customer_id": "2bf95490-db23-442d-a1bd-c6fd5efb584e"
}
A 413 Payload Too Large error should be returned when a file exceeds the server's limit, defined by the implementor. A reasonable size limit is 10MB, as most smartphone photos are around 3MB.
All other error responses should use the 400 Bad Request
status and contain details under the error
key.
{
"error": "'photo_id_front' cannot be decoded. Must be jpg or png."
}
GET [KYC_SERVER || TRANSFER_SERVER]/customer/files
One of the following parameters is required.
Name | Type | Description |
---|---|---|
file_id |
string | (optional) The file_id returned from a previous POST /customer/files request. The response's files list will contain a single object if this parameter is used. |
customer_id |
string | (optional) The id returned from a previous PUT /customer request. The response should include all files uploaded for the specified customer. |
Name | Type | Description |
---|---|---|
files |
array | A list file objects as described in the POST /customer/files response. |
{
"files": [
{
"file_id": "file_d5c67b4c-173c-428c-baab-944f4b89a57f",
"content_type": "image/png",
"size": 6134063,
"customer_id": "2bf95490-db23-442d-a1bd-c6fd5efb584e"
},
{
"file_id": "file_d3d54529-6683-4341-9b66-4ac7d7504238",
"content_type": "image/jpeg",
"size": 4089371,
"customer_id": "2bf95490-db23-442d-a1bd-c6fd5efb584e"
}
]
}
All responses should return 200 OK
. If no files are found for the identifer used, an empty list should be returned.
{
"files": []
}