Testing Managed Account Verification

A walkthrough of testing different verification states for Connect Managed Accounts using your test API key. If you need help after reading this, check out our answers to common questions or chat live with other developers in #stripe on freenode.

This document assumes you’re familiar with Managed Accounts overall, as well as updating accounts and identity verification.

Your platform is responsible for the identity verification process for its Managed Accounts. Connect streamlines this process by allowing Managed Accounts to be incrementally verified. Instead of requiring all information up front, Connect asks for a minimal set of information initially but limits how the account can be used (usually by blocking payouts past a certain limit). As the account provides Stripe with more information, we relax these limitations.

This incremental model of verification is split into stages. Once a managed account does a certain volume, a stage is “triggered”, meaning Stripe will ask for that stage’s information requirements. Once triggered, you have to provide the requested information within a limited time and additional charge volume. Once you’ve provided this information, a stage is completed. We allow you to provide information that may be required before a stage is triggered, which can minimize inconveniences.

The diagram above demonstrates the stages involved in incremental verification for U.S. Managed Accounts. Note that each country has different requirements.

In test mode, transitioning between the different stages is done with special credit card numbers.

Let’s manually go through the verification process for a new managed account. It’s recommended to follow this series of commands sequentially. Please note that multiple steps can be completed at the same time: for example, you can create an account with a name and date of birth, bank account, and terms of service acceptance info. This would immediately bypass the first stage of verification, and jump you right to step 5 in this flow.

1. Creating a managed account

Let’s start by creating a new test mode managed account, adding a bank account, and showing that the managed account holder has accepted the Stripe Services Agreement. Explicit Services Agreement acceptance is required for making transfers.

ACCT=$(curl https://api.stripe.com/v1/accounts \
   -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
   -d managed=true \
   -d country=US \
   -d external_account[object]=bank_account \
   -d external_account[country]=US \
   -d external_account[currency]=usd \
   -d external_account[routing_number]=110000000 \
   -d external_account[account_number]=000123456789 \
   -d tos_acceptance[date]=1481352974 \
   -d tos_acceptance[ip]="54.161.175.236"
)

ACCT_ID=$(echo $ACCT | python -c 'import json,sys;obj=json.load(sys.stdin);print obj["id"]')
echo $ACCT_ID
# Set your secret key: remember to change this to your live secret key in production
# See your keys here: https://dashboard.stripe.com/account/apikeys
Stripe.api_key = "sk_test_BQokikJOvBiI2HlWgH4olfQ2"

acct = Stripe::Account.create(
  {
    :country => 'US',
    :managed => true,
    :external_account => {
      :object => 'bank_account',
      :country => 'US',
      :currency => 'usd',
      :routing_number => '110000000',
      :account_number => '000123456789',
    },
    :tos_acceptance => {
      :date => 1481352974,
      :ip => '54.161.175.236',
    }
  }
)

acct_id = acct.id
puts acct_id
# Set your secret key: remember to change this to your live secret key in production
# See your keys here: https://dashboard.stripe.com/account/apikeys
stripe.api_key = "sk_test_BQokikJOvBiI2HlWgH4olfQ2"

acct = stripe.Account.create(
  managed=True,
  country='US',
  external_account=dict(
    object='bank_account',
    country='US',
    currency='usd',
    routing_number='110000000',
    account_number='000123456789',
  ),
  tos_acceptance=dict(
    date=1481352974,
    ip="54.161.175.236",
  ),
)

acct_id = acct.id
print acct_id
// Set your secret key: remember to change this to your live secret key in production
// See your keys here: https://dashboard.stripe.com/account/apikeys
\Stripe\Stripe::setApiKey("sk_test_BQokikJOvBiI2HlWgH4olfQ2");

$acct = \Stripe\Account::create(array(
  "managed" => true,
  "country" => "US",
  "external_account" => array(
    "object" => "bank_account",
    "country" => "US",
    "currency" => "usd",
    "routing_number" => "110000000",
    "account_number" => "000123456789",
  ),
  "tos_acceptance" => array(
    "date" => 1481352974,
    "ip" => "54.161.175.236"
  )
));

$acct_id = $acct->id;
echo $acct_id;
// Set your secret key: remember to change this to your live secret key in production
// See your keys here: https://dashboard.stripe.com/account/apikeys
var stripe = require("stripe")("sk_test_BQokikJOvBiI2HlWgH4olfQ2");

var acct;

stripe.accounts.create({
  managed: true,
  country: 'US',
  external_account: {
    object: "bank_account",
    country: "US",
    currency: "usd",
    routing_number: "110000000",
    account_number: "000123456789",
  },
  tos_acceptance: {
    date: 1481352974,
    ip: "54.161.175.236"
  }
}, function(err, account) {
  acct_id = account.id;
  console.log(acct_id);
});
// Set your secret key: remember to change this to your live secret key in production
// See your keys here: https://dashboard.stripe.com/account/apikeys
Stripe.apiKey = "sk_test_BQokikJOvBiI2HlWgH4olfQ2";

Map<String, Object> accountParams = new HashMap<String, Object>();
accountParams.put("managed", true);
accountParams.put("country", 'US');

Map<String, Object> externalAccountParams = new HashMap<String, Object>();
externalAccountParams.put("object", "bank_account");
externalAccountParams.put("country", "US");
externalAccountParams.put("currency", "usd");
externalAccountParams.put("routing_number", "110000000");
externalAccountParams.put("account_number", "000123456789");
accountParams.put("external_account", externalAccountParams);

Map<String, Object> tosParams = new HashMap<String, Object>();
tosParams.put("date", 1481352974);
tosParams.put("ip", "54.161.175.236");
accountParams.put("tos_acceptance", tosParams);

Account acct = Account.create(accountParams);
acctId = acct.id;
System.out.println(acctId);

2. First verification stage

No stage active

Initially, no stages are active. At this point you can make charges but not transfer funds. That is the starting state of all Managed Accounts.

Now you will trigger the account’s first stage with one of our test card numbers (4000 0000 0000 4202). This magic credit card simulates crossing the stage’s trigger threshold. Typically this happens after crossing a specific dollar amount in total charges.

curl https://api.stripe.com/v1/charges \
  -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
  -d amount=1000 \
  -d currency=usd \
  -d source[object]=card \
  -d source[number]=4000000000004202 \
  -d source[exp_month]=2 \
  -d source[exp_year]=2017 \
  -d destination="$ACCT_ID"
)

# Re-fetch the account to see what its status is.
curl https://api.stripe.com/v1/accounts/$ACCT_ID \
   -u sk_test_BQokikJOvBiI2HlWgH4olfQ2:

Stripe::Charge.create(
  :amount => 1000,
  :currency => "usd",
  :source => {
    :object => "card",
    :number => "4000000000004202",
    :exp_month => 2,
    :exp_year => 2017
  },
  :destination => acct_id
)

# Re-fetch the account to see what its status is.
Stripe::Account.retrieve(acct_id)
stripe.Charge.create(
  amount=1000,
  currency="usd",
  source={
    object="card",
    number="4000000000004202",
    exp_month=2,
    exp_year=2017
  },
  destination=acct_id
)
\Stripe\Charge::create(array(
  "amount" => 1000,
  "currency" => "usd",
  "source" => array(
    object => "card",
    number => "4000000000004202",
    exp_month => 2,
    exp_year => 2017
  ),
  "destination" => $acct_id
));
stripe.charges.create({
  amount: 1000,
  currency: "usd",
  source: {
    object: "card",
    number: "4000000000004202",
    exp_month: 2,
    exp_year: 2017
  },
  destination: acct_id
}, function(err, charge) {
  // asynchronously called
});
Map<String, Object> chargeParams = new HashMap<String, Object>();
chargeParams.put("amount", 1000);
chargeParams.put("currency", "usd");
chargeParams.put("destination", acctId);

Map<String, Object> sourceParams = new HashMap<String, Object>();
sourceParams.put("object", "card");
sourceParams.put("number", "4000000000004202");
sourceParams.put("exp_month", 2);
sourceParams.put("exp_year", 2017);
chargeParams.put("source", sourceParams); // obtained with Stripe.js

Charge.create(chargeParams);

You should now see that in addition to requesting additional information via verification[fields_needed], there’s now a deadline for providing this information in the verification[due_by] field.

Stage 1a Triggered

This is because you have triggered the first stage. In this stage, just as with the initial state, charges are enabled but transfers are disabled. You’ll see that charges_enabled on your account is still true: Stripe has given you a deadline to provide this information, but you have not passed it yet.

3. Triggering charge limits

While the stage allows charges at first, it has a limit to the volume of charges you can make while in that stage. To simulate crossing that limit, you will make a charge with another trigger card number (4000 0000 0000 4210).

curl https://api.stripe.com/v1/charges \
  -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
  -d amount=1000 \
  -d currency=usd \
  -d source[object]=card \
  -d source[number]=4000000000004210 \
  -d source[exp_month]=2 \
  -d source[exp_year]=2017 \
  -d destination="$ACCT_ID"
)
Stripe::Charge.create(
  :amount => 1000,
  :currency => "usd",
  :source => {
    :object => "card",
    :number => "4000000000004210",
    :exp_month => 2,
    :exp_year => 2017
  },
  :destination => acct_id
)
stripe.Charge.create(
  amount=1000,
  currency="usd",
  source={
    object="card",
    number="4000000000004210",
    exp_month=2,
    exp_year=2017
  },
  destination=acct_id
)
\Stripe\Charge::create(array(
  "amount" => 1000,
  "currency" => "usd",
  "source" => array(
    object => "card",
    number => "4000000000004210",
    exp_month => 2,
    exp_year => 2017
  ),
  "destination" => $acct_id
));
stripe.charges.create({
  amount: 1000,
  currency: "usd",
  source: {
    object: "card",
    number: "4000000000004210",
    exp_month: 2,
    exp_year: 2017
  },
  destination: acct_id
}, function(err, charge) {
  // asynchronously called
});
Map<String, Object> chargeParams = new HashMap<String, Object>();
chargeParams.put("amount", 1000);
chargeParams.put("currency", "usd");
chargeParams.put("destination", acctId);

Map<String, Object> sourceParams = new HashMap<String, Object>();
sourceParams.put("object", "card");
sourceParams.put("number", "4000000000004210");
sourceParams.put("exp_month", 2);
sourceParams.put("exp_year", 2017);
chargeParams.put("source", sourceParams); // obtained with Stripe.js

Charge.create(chargeParams);

Stage 1b Triggered

Now you are not able to make new charges, and the account has been updated to show both charges_enabled as false and verification[disabled_reason] as fields_needed.

4. Fulfilling the first stage

To know what fields you need to provide at this stage, you should first get a list of necessary fields.

curl https://api.stripe.com/v1/accounts/$ACCT_ID \
   -u sk_test_BQokikJOvBiI2HlWgH4olfQ2:

puts(Stripe::Account.retrieve(acct_id))
print stripe.Account.retrieve(acct_id)
var_dump(\Stripe\Account::retrieve($acct_id));
stripe.accounts.retrieve(
  acct_id,
  function(err, account) {
    console.log(account);
  }
);
System.out.println(Account.retrieve(acctId, null));

You’ll see that in the U.S. these verification[fields_needed] are first_name, last_name, and dob.

Stripe must collect this initial set of fields in order to satisfy our OFAC requirements. Because of the nature of these checks, if they are not provided Stripe must block charges for the account. While you can charge a certain amount before collecting name and date of birth, since charge blocking is so disruptive to a business, we highly recommend collecting these fields upfront to avoid any potential issues.

curl https://api.stripe.com/v1/accounts/$ACCT_ID \
   -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
   -d legal_entity[dob][day]=10 \
   -d legal_entity[dob][month]=1 \
   -d legal_entity[dob][year]=1986 \
   -d legal_entity[first_name]=Jenny \
   -d legal_entity[last_name]=Rosen \
   -d legal_entity[type]=individual

account = Stripe::Account.retrieve(acct_id)
account.legal_entity.dob.day = 10
account.legal_entity.dob.month = 01
account.legal_entity.dob.year = 1986
account.legal_entity.first_name = "Jenny"
account.legal_entity.last_name = "Rosen"
account.legal_entity.type = "individual"
account.save
account = stripe.Account.retrieve(acct_id)
account.legal_entity.dob.day = 10
account.legal_entity.dob.month = 01
account.legal_entity.dob.year = 1986
account.legal_entity.first_name = "Jenny"
account.legal_entity.last_name = "Rosen"
account.legal_entity.type = "individual"
account.save()
$account = \Stripe\Account::retrieve($acct_id);
$account->legal_entity->dob->day = 10;
$account->legal_entity->dob->month = 01;
$account->legal_entity->dob->year = 1986;
$account->legal_entity->first_name = "Jenny";
$account->legal_entity->last_name = "Rosen";
$account->legal_entity->type = "individual";
$account->save();
stripe.accounts.update(acct_id, {
  legal_entity: {
    dob: {
      day: 10,
      month: 1,
      year: 1986
    },
    first_name: "Jenny",
    last_name: "Rosen",
    type: "individual"
  }
});
Account account = Account.retrieve(acctId, null);
Map<String, Object> params = new HashMap<String, Object>();
Map<String, Object> legalEntityParams = new HashMap<String, Object>();
Map<String, Object> dobParams = new HashMap<String, Object>();
dobParams.put("day", 10);
dobParams.put("month", 1);
dobParams.put("year", 1986);
legalEntityParams.put("dob", dobParams)
legalEntityParams.put("first_name", "Jenny");
legalEntityParams.put("last_name", "Rosen");
legalEntityParams.put("type", "individual");
params.put("legal_entity", legalEntityParams);
account.update(params);

Stage 1 Resolved

Once you provide this information, Stripe immediately re-enables charges, and enables transfers. You’ll also notice that while verification[fields_needed] is still set (with different values,) verification[due_by] is not. This means that while Stripe will require more information in the future, it’s not immediately required, and you are able to defer providing it until the account has charged more.

5. Second stage

Let’s go ahead and trigger the next stage using the trigger threshold magic card.

curl https://api.stripe.com/v1/charges \
  -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
  -d amount=1000 \
  -d currency=usd \
  -d source[object]=card \
  -d source[number]=4000000000004202 \
  -d source[exp_month]=2 \
  -d source[exp_year]=2017 \
  -d destination="$ACCT_ID"
)

# Re-fetch the account to see what its status is.
curl https://api.stripe.com/v1/accounts/$ACCT_ID \
   -u sk_test_BQokikJOvBiI2HlWgH4olfQ2:

Stripe::Charge.create(
  :amount => 1000,
  :currency => "usd",
  :source => {
    :object => "card",
    :number => "4000000000004202",
    :exp_month => 2,
    :exp_year => 2017
  },
  :destination => acct_id
)

# Re-fetch the account to see what its status is.
puts(Stripe::Account.retrieve(acct_id))
stripe.Charge.create(
  amount=1000,
  currency="usd",
  source={
    object="card",
    number="4000000000004202",
    exp_month=2,
    exp_year=2017
  },
  destination=acct_id
)

# Re-fetch the account to see what its status is.
print stripe.Account.retrieve(acct_id)
\Stripe\Charge::create(array(
  "amount" => 1000,
  "currency" => "usd",
  "source" => array(
    object => "card",
    number => "4000000000004202",
    exp_month => 2,
    exp_year => 2017
  ),
  "destination" => $acct_id
));

// Re-fetch the account to see what its status is.
var_dump(\Stripe\Account::retrieve($acct_id));
stripe.charges.create({
  amount: 1000,
  currency: "usd",
  source: {
    object: "card",
    number: "4000000000004202",
    exp_month: 2,
    exp_year: 2017
  },
  destination: acct_id
}, function(err, charge) {
  stripe.accounts.retrieve(
    acct_id,
    function(err, account) {
      console.log(account);
    }
  );
);

});
Map<String, Object> chargeParams = new HashMap<String, Object>();
chargeParams.put("amount", 1000);
chargeParams.put("currency", "usd");
chargeParams.put("destination", acctId);

Map<String, Object> sourceParams = new HashMap<String, Object>();
sourceParams.put("object", "card");
sourceParams.put("number", "4000000000004202");
sourceParams.put("exp_month", 2);
sourceParams.put("exp_year", 2017);
chargeParams.put("source", sourceParams);

Charge.create(chargeParams);

System.out.println(Account.retrieve(acctId));

Stage 2a Triggered

You can still charge and transfer, just as before, but now verification[due_by] on the account is set, meaning you need to provide more information in the near future.

The worst that can happen in this stage is that Stripe will disable transfers (again, the only time Stripe disables charges due to verification is in the first stage).

6. Triggering transfer limits

While this stage does not have a charge-disabling limit, it does have a transfer-disabling limit. You can trigger a transfer limit using a trigger card number (4000 0000 0000 4236). This is similar to the charge limit, but it blocks transfers.

curl https://api.stripe.com/v1/charges \
  -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
  -d amount=1000 \
  -d currency=usd \
  -d source[object]=card \
  -d source[number]=4000000000004236 \
  -d source[exp_month]=2 \
  -d source[exp_year]=2017 \
  -d destination="$ACCT_ID"
)
Stripe::Charge.create(
  :amount => 1000,
  :currency => "usd",
  :source => {
    :object => "card",
    :number => "4000000000004236",
    :exp_month => 2,
    :exp_year => 2017
  },
  :destination => acct_id
)
stripe.Charge.create(
  amount=1000,
  currency="usd",
  source={
    object="card",
    number="4000000000004236",
    exp_month=2,
    exp_year=2017
  },
  destination=acct_id
)
\Stripe\Charge::create(array(
  "amount" => 1000,
  "currency" => "usd",
  "source" => array(
    object => "card",
    number => "4000000000004236",
    exp_month => 2,
    exp_year => 2017
  ),
  "destination" => $acct_id
));
stripe.charges.create({
  amount: 1000,
  currency: "usd",
  source: {
    object: "card",
    number: "4000000000004236",
    exp_month: 2,
    exp_year: 2017
  },
  destination: acct_id
}, function(err, charge) {

});
Map<String, Object> chargeParams = new HashMap<String, Object>();
chargeParams.put("amount", 1000);
chargeParams.put("currency", "usd");
chargeParams.put("destination", acctId);

Map<String, Object> sourceParams = new HashMap<String, Object>();
sourceParams.put("object", "card");
sourceParams.put("number", "4000000000004236");
sourceParams.put("exp_month", 2);
sourceParams.put("exp_year", 2017);
chargeParams.put("source", sourceParams);

Charge.create(chargeParams);

Stage 2b Triggered

After this trigger, all transfers should fail.

# Will return an error
curl https://api.stripe.com/v1/transfers \
  -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
  -H Stripe-Account="$ACCT_ID" \
  -d amount=400 \
  -d currency=usd \
  -d recipient=self
)
Stripe::Transfer.create(
  {
    :amount => 400,
    :currency => "usd",
    :recipient => "self",
  },
  {
    :stripe_account => acct_id
  }
)
stripe.Transfer.create(
  amount=400,
  currency="usd",
  recipient="self",
  stripe_account=acct_id
)
\Stripe\Transfer::create(
  array(
    "amount" => 400,
    "currency" => "usd",
    "recipient" => "self"
  ),
  array(
    "stripe_account" => $acct_id
  )
);
stripe.transfers.create({
  amount: 400,
  currency: "usd",
  recipient: "self"
}, {
  stripe_account: acct_id
}, function(err, transfer) {
  // asynchronously called
});
RequestOptions requestOptions = RequestOptions.builder().setStripeAccount(acctId).build();

Map<String, Object> transferParams = new HashMap<String, Object>();
transferParams.put("amount", 400);
transferParams.put("currency", "usd");
transferParams.put("recipient", "self");

Transfer.create(transferParams, requestOptions);

7. Fulfilling the second stage

curl https://api.stripe.com/v1/accounts/$ACCT_ID \
   -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
   -d legal_entity[address][line1]="1234 Main Street" \
   -d legal_entity[address][postal_code]=94111 \
   -d legal_entity[address][city]="San Francisco" \
   -d legal_entity[address][state]=CA \
   -d legal_entity[ssn_last_4]=1234

account = Stripe::Account.retrieve(acct_id)
account.legal_entity.address.line1 = "1234 Main Street"
account.legal_entity.address.postal_code = 94111
account.legal_entity.address.city = "San Francisco"
account.legal_entity.address.state = "CA"
account.legal_entity.ssn_last_4 = 1234
account.save
account = stripe.Account.retrieve(acct_id)
account.legal_entity.address.line1 = "1234 Main Street"
account.legal_entity.address.postal_code = 94111
account.legal_entity.address.city = "San Francisco"
account.legal_entity.address.state = "CA"
account.legal_entity.ssn_last_4 = 1234
account.save()
$account = \Stripe\Account::retrieve($acct_id);
$account->legal_entity->address.line1 = "1234 Main Street";
$account->legal_entity->address.postal_code = 94111;
$account->legal_entity->address.city = "San Francisco";
$account->legal_entity->address.state = "CA";
$account->legal_entity->ssn_last_4 = 1234;
$account->save();
stripe.accounts.update(acct_id, {
  legal_entity: {
    address: {
      line1: "1234 Main Street",
      postal_code: 94111,
      city: "San Francisco",
      state: "CA"
    },
    ssn_last_4: 1234
  }
);
Account account = Account.retrieve(acctId, null);
Map<String, Object> params = new HashMap<String, Object>();
Map<String, Object> legalEntityParams = new HashMap<String, Object>();
Map<String, Object> addressParams = new HashMap<String, Object>();
addressParams.put("line1", "1234 Main Street");
addressParams.put("postal_code", 94111);
addressParams.put("city", "San Francisco");
addressParams.put("state", "CA");
legalEntityParams.put("address", addressParams)
legalEntityParams.put("ssn_last_4", 1234);
params.put("legal_entity", legalEntityParams);
account.update(params);

Stage 2 Resolved

After fulfilling the stage, transfers work again.

curl https://api.stripe.com/v1/transfers \
  -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
  -H Stripe-Account="$ACCT_ID" \
  -d amount=400 \
  -d currency=usd \
  -d recipient=self
)
Stripe::Transfer.create(
  {
    :amount => 400,
    :currency => "usd",
    :recipient => "self",
  },
  {
    :stripe_account => acct_id
  }
)
stripe.Transfer.create(
  amount=400,
  currency="usd",
  recipient="self",
  stripe_account=acct_id
)
\Stripe\Transfer::create(
  array(
    "amount" => 400,
    "currency" => "usd",
    "recipient" => "self"
  ),
  array(
    "stripe_account" => $acct_id
  )
);
stripe.transfers.create({
  amount: 400,
  currency: "usd",
  recipient: "self"
}, {
  stripe_account: acct_id
}, function(err, transfer) {
  // asynchronously called
});
RequestOptions requestOptions = RequestOptions.builder().setStripeAccount(acctId).build();

Map<String, Object> transferParams = new HashMap<String, Object>();
transferParams.put("amount", 400);
transferParams.put("currency", "usd");
transferParams.put("recipient", "self");

Transfer.create(transferParams, requestOptions);

8. Fulfilling multiple stages

As noted before, providing the right information can fulfill multiple stages. Let’s try to complete the last two stages all at once.

First, obtain the test success image by running:

wget https://stripe.com/img/documentation/guides/testing/success.png

From here, you can upload the test success image.

FILE_OBJ=$(curl https://uploads.stripe.com/v1/files \
  -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
  -H Stripe-Account="$ACCT_ID" \
  -F purpose=identity_document \
  -F file="@success.png"
)

FILE=$(echo $FILE_OBJ | python -c 'import json,sys;obj=json.load(sys.stdin);print obj["id"]')
file_obj = Stripe::FileUpload.create(
  {
    :purpose => 'identity_document',
    :file => File.new('success.png')
  },
  {
    :stripe_account => acct_id
  }
)
file = file_obj.id
fp = open('success.png', 'r')
file_obj = stripe.FileUpload.create(
  purpose='identity_document',
  file=fp,
  stripe_account=acct_id
)
file = file_obj.id
$fp = fopen('success.png', 'r');
$file_obj = \Stripe\FileUpload::create(
  array(
    "purpose" => "identity_document",
    "file" => $fp
  ),
  array(
    "stripe_account" => $acct_id
  )
));
$file = $file_obj->id;
var fp = fs.readFileSync('success.png');
var file;
stripe.fileUploads.create({
  purpose: 'identity_document',
  file: {
    data: fp,
    name: 'success.png',
    type: 'application/octet-stream'
  }
}, function(err, fileUpload) {
  file = fileUpload.id;
});
Map<String, Object> fileUploadParams = new HashMap<String, Object>();
fileUploadParams.put("purpose", "identity_document");
fileUploadParams.put("file", new File('success.png'));

FileUpload fileObj = FileUpload.create(fileUploadParams);
file = fileObj.id;

Once the image is uploaded, you can update the account with both pieces of verification information.

curl https://api.stripe.com/v1/accounts/$ACCT_ID \
  -u sk_test_BQokikJOvBiI2HlWgH4olfQ2: \
  -d legal_entity[personal_id_number]=123456789 \
  -d legal_entity[verification][document]="$FILE"
)
account = Stripe::Account.retrieve(acct_id)
account.legal_entity.personal_id_number = 123456789
account.legal_entity.verification.document = file
account.save
account = stripe.Account.retrieve(acct_id)
account.legal_entity.personal_id_number = 123456789
account.legal_entity.verification.document = file
account.save()
$account = \Stripe\Account::retrieve($acct_id);
$account->legal_entity->personal_id_number = 123456789;
$account->legal_entity->verification->document = $file;
$account->save();
stripe.accounts.update(acct_id, {
  legal_entity: {
    personal_id_number: 123456789,
    verification: {
      document: file
    }
  }
);
Account account = Account.retrieve(acctId, null);
Map<String, Object> params = new HashMap<String, Object>();
Map<String, Object> legalEntityParams = new HashMap<String, Object>();
Map<String, Object> verificationParams = new HashMap<String, Object>();
verificationParams.put("document", file);
legalEntityParams.put("verification", verificationParams)
legalEntityParams.put("personal_id_number", 123456789);
params.put("legal_entity", legalEntityParams);
account.update(params);

This single update will actually jump through two stages, from stage 2 fulfilled to complete. You can confirm verification with a GET account call.

curl https://api.stripe.com/v1/accounts/$ACCT_ID \
   -u sk_test_BQokikJOvBiI2HlWgH4olfQ2:

puts(Stripe::Account.retrieve(acct_id))
print stripe.Account.retrieve(acct_id)
var_dump(\Stripe\Account::retrieve($acct_id));
stripe.accounts.retrieve(
  acct_id,
  function(err, account) {
    console.log(account);
  }
);
Account.retrieve(acctId, null);

You’ll see that verification[fields_needed] is now empty, meaning Stripe will not need any additional information for this account unless a significant exception happens (for example, if the account appears to be used for fraud).