Handling Identity Verification with the API

    Learn how Connect platforms can use webhooks and the API to handle identity verification of their Custom Accounts.

    Platforms with Custom accounts are required to provide Stripe with the necessary information about their users to meet “Know Your Customer” (KYC) regulations. As a Connect platform with Custom accounts, it’s your responsibility to collect the required information from your users and provide it to Stripe. We’ll then perform verification, asking for more information when needed.

    The rest of this page goes through how platforms:

    • Know when identity verification is needed
    • Provide the necessary information to Stripe

    You should also read our Identity Verification for Custom Accounts guide to learn about the verification flow options, how our API fields translate to both companies and individuals, and how to localize information requests.

    Verification process

    Before payouts can be enabled for a connected account, Stripe needs a certain set of information. The specific needs vary depending on the country and legal entity type of the connected account.

    Platforms need to choose the proper verification flow for their business and users to meet the KYC requirements. Broadly speaking, this means providing all the requisite information up front or on demand. Either way, you’ll need to be set up to watch for and respond to requests from Stripe.

    1. Establish a platform webhook URL in your webhook settings to watch for activity, especially events of the account.updated type.
    2. Immediately after creating an account, check the Account object’s verification[fields_needed] property for any additional requirements. If additional information is required, obtain it from the user and update the connected account.
    3. Continue watching for account.updated event notifications to see if verification[fields_needed] changes, reaching out to your user for additional information as needed.

    Note that when you provide additional information, you do not need to resubmit any previously verified details (e.g., if the dob has already been verified, it does not need to be provided again in subsequent updates).

    Once an individual is verified, you cannot change that individual’s information. For verified companies, you’ll need to contact us to make changes (e.g., the name or EIN on file).

    Determining if identity verification is needed

    When you receive an account.updated webhook notification or fetch an account via the API, you receive an Account object. The Account object’s charges_enabled and payouts_enabled properties reflect the account’s current capabilities.

    The Account object has a verification property, a read-only hash representing the requirements needed to verify the account. The verification hash has a fields_needed property: an array of strings representing the account fields required for verification. These strings usually correlate directly to Account object properties. For example, the string legal_entity.type corresponds to the type field in the legal_entity hash.

    {
      "verification": {
        "fields_needed": [
          "legal_entity.type",
        ],
        "due_by": null,
        "disabled_reason": null,
      },
      ...
    }

    There are a few special fields_needed values that warrant noting:

    • legal_entity.additional_owners, see the additional_owners section below for information on the EU owner requirements
    • legal_entity.verification.document and legal_entity.additional_owners.[#].verification.document, explained in the next section
    • external_account (noting the singular), means the account needs an attached destination for payouts to complete activation

    If fields_needed is not an empty array, due_by may be set. This is a Unix timestamp identifying when the information is needed by. Usually, if we don’t receive the information we need by the due date, we will disable payouts on the account. However, there can be other consequences for rarer situations. For example, if payouts are already disabled and our inquiries are not being responded to within a reasonable period of time, Stripe may also disable the ability to process charges. Unless fraud or other rare circumstances occur, Stripe will always provide at least 3 days for the information to be provided before limiting account functionality.

    Separately, the disabled_reason property may also be set. This is a string describing the reason why this account is unable to make payouts or charges. The reason can fall into several categories.

    Reason Meaning
    fields_needed Additional verification information is required to enable payout or charge capabilities on this account
    listed Account might be a match on a prohibited persons or companies list (Stripe will investigate and either reject or reinstate the account appropriately)
    rejected.fraud Account is rejected due to suspected fraud or illegal activity
    rejected.listed Account is rejected due to a match on a third-party prohibited persons or companies list (such as financial services provider or government)
    rejected.terms_of_service Account is rejected due to suspected terms of service violations
    rejected.other Account is rejected for another reason
    under_review Account is under review by Stripe
    other Account is not rejected but is disabled for another reason while being reviewed

    Handling ID verification problems

    Many complications with the legal entity verification process involve the ID verification itself. To help you recognize and handle the most common problems, this table revisits the above list of errors specific to the ID, presenting the likely resolutions.

    Error Resolution
    scan_corrupt
    scan_failed_greyscale
    scan_not_readable
    scan_not_uploaded
    The upload failed due to a problem with the file itself. Ask your user to provide a new file that meets the requirements: color, JPG or PNG, and less than 8MB.
    scan_id_country_not_supported
    scan_id_type_not_supported
    The provided file was not a government-issued ID from a supported country. Ask your user to provide a new file that meets that requirement.
    scan_name_mismatch The name on the ID does not match the name on the account. Ask your users to verify and correct the provided name information on the account.
    failed_keyed_identity The name on the account could not be verified. Ask your users to verify they provided their full legal name and provide a photo ID matching that name.
    failed_other
    scan_failed_other
    Your support team should reach out to Stripe to learn more about why the identity verification failed.

    Additional owners

    The legal_entity[additional_owners] parameter is an array for providing details about other owners of the company, aside from the legal representative.

    For Hong Kong, Singapore, and Single Euro Payments Area member countries, Stripe is required to collect and verify information about anybody that owns at least 25% of the company. For other countries, the field is entirely optional, although you can still provide values for it.

    For each owner, the possible fields are:

    • first_name
    • last_name
    • dob
    • address

    The address and dob fields are identical in formatting to the top-level legal_entity[address] and legal_entity[dob] fields. The legal entity’s address does not need to be in the same country as the Stripe account.

    Stripe attempts to verify each owner, and if the verification fails, we may request more information. As such, each additional owner has a verification property that works exactly the same as the legal_entity[verification] hash (see identity verification above), but for that individual.

    When legal_entity.additional_owners is listed in the verification[fields_needed] array, you must ask the user to provide any additional owners. You may pass an array or integer-indexed hash with information on additional owners.

    If there are no additional owners, update the legal_entity[additional_owners] parameter as shown in the bottom example below.

    # Create or update additional owners
    curl https://api.stripe.com/v1/accounts/{CONNECTED_STRIPE_ACCOUNT_ID} \
       -u {PLATFORM_SECRET_KEY}: \
       -d legal_entity[additional_owners][0][first_name]=Bob \
       -d legal_entity[additional_owners][0][last_name]=Smith \
       -d legal_entity[additional_owners][1][first_name]=Jane \
       -d legal_entity[additional_owners][1][last_name]=Doe
    
    # Indicate that there are no additional owners
    curl https://api.stripe.com/v1/accounts/{CONNECTED_STRIPE_ACCOUNT_ID} \
       -u {PLATFORM_SECRET_KEY}: \
       -d legal_entity[additional_owners]=
    
    Stripe.api_key = PLATFORM_SECRET_KEY
    account = Stripe::Account.retrieve({CONNECTED_STRIPE_ACCOUNT_ID})
    
    # Create additional owners
    account.legal_entity.additional_owners = [
      {:first_name => 'Bob', :last_name => 'Smith'},
      {:first_name => 'Jane', :last_name => 'Doe'}
    ]
    
    # Add an additional owner
    length = account.legal_entity.additional_owners.length
    account.legal_entity.additional_owners[length] = {
      :first_name => 'Andrew',
      :last_name => 'Jackson'
    }
    
    # Update additional owners
    account.legal_entity.additional_owners[0].first_name = 'Robert'
    
    # Indicate that there are no additional owners
    account.legal_entity.additional_owners = nil
    
    account.save
    
    stripe.api_key = PLATFORM_SECRET_KEY
    account = stripe.Account.retrieve({CONNECTED_STRIPE_ACCOUNT_ID})
    
    # Create additional owners
    account.legal_entity.additional_owners = [
      {"first_name": "Bob", "last_name": "Smith"},
      {"first_name": "Jane", "last_name": "Doe"}
    ]
    
    # Add an additional owner
    account.legal_entity.additional_owners.append(
      {"first_name": "Andrew", "last_name": "Jackson"}
    )
    
    # Update additional owners
    account.legal_entity.additional_owners[0].first_name = "Robert"
    
    # Indicate that there are no additional owners
    account.legal_entity.additional_owners = None
    
    account.save()
    
    \Stripe\Stripe::setApiKey(PLATFORM_SECRET_KEY);
    $account = \Stripe\Account::retrieve({CONNECTED_STRIPE_ACCOUNT_ID});
    
    // Create additional owners
    $account->legal_entity->additional_owners = array(
      array('first_name' => 'Bob', 'last_name' => 'Smith'),
      array('first_name' => 'Jane', 'last_name' => 'Doe')
    );
    
    // Add an additional owner
    $count = count($account->legal_entity->additional_owners);
    $account->legal_entity->additional_owners[$count] = array(
      'first_name' => 'Andrew',
      'last_name' => 'Jackson'
    );
    
    // Update additional owners
    $account->legal_entity->additional_owners[0]->first_name = 'Robert';
    
    // Indicate that there are no additional owners
    $account->legal_entity->additional_owners = null;
    
    $account->save();
    
    Stripe.apiKey = PLATFORM_SECRET_KEY;
    Account account = Account.retrieve({CONNECTED_STRIPE_ACCOUNT_ID}, (RequestOptions) null);
    
    Map<String, Object> accountParams = new HashMap<String, Object>();
    Map<String, Object> legalEntityParams = new HashMap<String, Object>();
    List<Map<String, Object>> additionalOwnersParams = new LinkedList<Map<String, Object>>();
    
    // Create additional owners
    Map<String, Object> newOwner1 = new HashMap<String, Object>();
    newOwner1.put("first_name", "Bob");
    newOwner1.put("last_name", "Smith");
    
    Map<String, Object> newOwner2 = new HashMap<String, Object>();
    newOwner2.put("first_name", "Jane");
    newOwner2.put("last_name", "Doe");
    
    additionalOwnersParams.add(newOwner1);
    additionalOwnersParams.add(newOwner2);
    
    // Add an additional owner
    int size = additionalOwnersParams.size();
    Map<String, Object> newOwner3 = new HashMap<String, Object>();
    newOwner3.put("first_name", "Andrew");
    newOwner3.put("last_name", "Jackson");
    additionalOwnersParams.add(size, newOwner3);
    
    // Update additional owners
    additionalOwnersParams.get(0).put("first_name", "Robert");
    
    // Indicate that there are no additional owners
    legalEntityParams.put("additional_owners", new LinkedList<Object>());
    
    accountParams.put("legal_entity", legalEntityParams);
    
    account.update(accountParams);
    
    var stripe = require('stripe')(PLATFORM_SECRET_KEY);
    
    // Create and update additional owners
    stripe.accounts.update(
      {CONNECTED_STRIPE_ACCOUNT_ID},
      {
        legal_entity: {
          additional_owners: {
            // Note the use of an object instead of an array
            0: {first_name: 'Bob', last_name: 'Smith'},
            1: {first_name: 'Jane', last_name: 'Doe'}
          }
        }
      }
    );
    
    // Indicate that there are no additional owners
    stripe.accounts.update(
      {CONNECTED_STRIPE_ACCOUNT_ID},
      {legal_entity: {additional_owners: ''}}
    );
    
    stripe.Key = PLATFORM_SECRET_KEY
    
    // Create and update additional owners
    params := &stripe.AccountParams{
      LegalEntity: &stripe.LegalEntity{
        AdditionalOwners: []stripe.Owner{
          {First: "Bob", Last: "Smith"},
          {First: "Jane", Last: "Doe"},
        },
      },
    }
    acct, err := account.Update("{CONNECTED_STRIPE_ACCOUNT_ID}", params)
    
    // Indicate that there are no additional owners
    params := &stripe.AccountParams{
      LegalEntity: &stripe.LegalEntity{
        AdditionalOwnersEmpty: true,
      },
    }
    acct, err := account.Update("{CONNECTED_STRIPE_ACCOUNT_ID}", params)
    

    Handling identity verification

    There are two ways to respond to an identity verification request of a legal entity. The first is to perform an update account call, correcting one of the legal entity’s first_name, last_name, dob hash, personal_id_number, or ssn_last_4 attributes. It’s possible that the account holder mis-entered this information, so Stripe allows you to change it.

    Secondarily, we may ask you to upload a color scan or photo of government-issued identification, such as a passport or driver’s license. This is a two-step process:

    1. Upload the file to Stripe
    2. Attach the file to the account

    For security reasons, we do not accept copies of IDs over email.

    Uploading a file

    To upload a file (a color image, as either a JPEG or PNG), POST it as part of a multipart/form-data request to https://files.stripe.com/v1/files. The maximum allowed file size is 8MB.

    Pass the file in the file parameter and provide a purpose parameter with the value identity_document:

    curl https://files.stripe.com/v1/files \
       -u {PLATFORM_SECRET_KEY}: \
       -H "Stripe-Account: {CONNECTED_STRIPE_ACCOUNT_ID}" \
       -F purpose=identity_document \
       -F file="@/path/to/a/file"
    
    Stripe.api_key = PLATFORM_SECRET_KEY
    Stripe::FileUpload.create(
      {
        :purpose => 'identity_document',
        :file => File.new('/path/to/a/file.jpg')
      },
      {:stripe_account => CONNECTED_STRIPE_ACCOUNT_ID}
    )
    
    stripe.api_key = PLATFORM_SECRET_KEY
    with open("/path/to/a/file.jpg", "r") as fp:
      stripe.FileUpload.create(
        purpose="identity_document",
        file=fp,
        stripe_account=CONNECTED_STRIPE_ACCOUNT_ID
      )
    
    \Stripe\Stripe::setApiKey(PLATFORM_SECRET_KEY);
    \Stripe\FileUpload::create(
      array(
        "purpose" => "identity_document",
        "file" => fopen('/path/to/a/file.jpg', 'r')
      ),
      array("stripe_account" => CONNECTED_STRIPE_ACCOUNT_ID)
    );
    
    Stripe.apiKey = PLATFORM_SECRET_KEY;
    RequestOptions requestOptions = RequestOptions.builder().setStripeAccount(CONNECTED_STRIPE_ACCOUNT_ID).build();
    
    Map<String, Object> fileUploadParams = new HashMap<String, Object>();
    fileUploadParams.put("purpose", "identity_document");
    fileUploadParams.put("file", new File("/path/to/a/file.jpg"));
    
    FileUpload.create(fileUploadParams, requestOptions);
    
    var fs = require('fs');
    var stripe = require('stripe')(PLATFORM_SECRET_KEY);
    stripe.fileUploads.create(
      {
        purpose: 'identity_document',
        file: {
          data: fs.readFileSync('/path/to/a/file.jpg'),
          name: 'file_name.jpg',
          type: 'application/octet-stream'
        }
      },
      {stripe_account: CONNECTED_STRIPE_ACCOUNT_ID}
    );
    
    file, err := os.Open("img/success.png")
    params := &stripe.FileUploadParams{
      FileReader: bufio.NewReader(file),
      Filename: "success.png",
      Purpose: "identity_document",
    }
    params.SetStripeAccount("{CONNECTED_STRIPE_ACCOUNT_ID}")
    file_upload, err := fileupload.New(params)
    

    This request uploads the file and returns a token:

    {
      "id": "file_5dtoJkOhAxrMWb",
      "created": 1403047735,
      "size": 4908
    }

    You may then use the token’s id value to attach the file to a connected account for identity verification.

    Attaching the file

    Once the file has been uploaded and a representative token returned, provide the file ID as the value of the document field in an update account call:

    curl https://api.stripe.com/v1/accounts/{CONNECTED_STRIPE_ACCOUNT_ID} \
       -u {PLATFORM_SECRET_KEY}: \
       -d legal_entity[verification][document]=file_5dtoJkOhAxrMWb
    
    Stripe.api_key = PLATFORM_SECRET_KEY
    account = Stripe::Account.retrieve({CONNECTED_STRIPE_ACCOUNT_ID})
    account.legal_entity.verification.document = 'file_5dtoJkOhAxrMWb'
    account.save
    
    stripe.api_key = PLATFORM_SECRET_KEY
    account = stripe.Account.retrieve({
      key: })
    account.legal_entity.verification.document = "file_5dtoJkOhAxrMWb"
    account.save()
    
    \Stripe\Stripe::setApiKey(PLATFORM_SECRET_KEY);
    $account = \Stripe\Account::retrieve({CONNECTED_STRIPE_ACCOUNT_ID});
    $account->legal_entity->verification->document = 'file_5dtoJkOhAxrMWb';
    $account->save();
    
    Stripe.apiKey = PLATFORM_SECRET_KEY;
    Account account = Account.retrieve({CONNECTED_STRIPE_ACCOUNT_ID}, (RequestOptions) null);
    
    Map<String, String> verificationParams = new HashMap<String, String>();
    verificationParams.put("document", "file_5dtoJkOhAxrMWb");
    
    Map<String, Object> legalEntityParams = new HashMap<String, Object>();
    legalEntityParams.put("verification", verificationParams);
    
    Map<String, Object> accountParams = new HashMap<String, Object>();
    accountParams.put("legal_entity", legalEntityParams);
    
    account.update(accountParams);
    
    var stripe = require('stripe')(PLATFORM_SECRET_KEY);
    stripe.accounts.update(
      {CONNECTED_STRIPE_ACCOUNT_ID},
      {legal_entity: {verification: {document: 'file_5dtoJkOhAxrMWb'}}}
    );
    
    params := &stripe.AccountParams{
      LegalEntity: &stripe.LegalEntity{
        Verification: stripe.IdentityVerification{
          Document: &stripe.IdentityDocument{
            ID: file_upload.ID,
          },
        },
      },
    }
    acct, err := account.Update("{CONNECTED_STRIPE_ACCOUNT_ID}", params)
    

    This step changes the legal_entity[verification][status] to pending. If an additional owner needs to be verified, instead use legal_entity[additional_owners][#][verification][document], where # is the index of the owner within the legal_entity[additional_owners] array.

    Confirming ID verification

    Assuming the photo of the ID passes Stripe’s checks, the status field changes to verified. You also receive an account.updated webhook notification upon completion of the verification process. Note that verification could take anywhere from a few minutes to a couple business days, depending on how readable the provided image is.

    If the verification attempt fails, the status is unverified and the details field contains a message stating the cause. The message is safe to present to your user, such as “The image supplied was not readable”. In addition, the response contains a details_code value, such as scan_not_readable. Upon failure, verification[fields_needed] indicates that a new ID upload is required. If the deadline for verification is near, verification[due_by] may also be populated with a date. Again, an account.updated webhook notification is sent as well.

    You can also know that an uploaded ID failed verification on an account if legal_entity[verification][document] has a value, but verification[fields_needed] indicates an ID is still required.

    Further reading

    Now that you know how to perform identity verification through the API, you may want to read: