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.
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 Terms of Service (ToS.) Explicit ToS acceptance is required for making transfers.
# 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=$(<pre class="language-bash"><code class="language-bash">curl <span class='token string'>https://api.stripe.com/v1/accounts</span> \
-u <span class='token string'>"$STRIPE_API_KEY:"</span> \
-d <span class='token keyword'>managed</span>=<span class='token string'>true</span> \
-d <span class='token keyword'>country</span>=<span class='token string'>US</span> \
-d <span class='token keyword'>external_accounts[object]</span>=<span class='token string'>bank_account</span> \
-d <span class='token keyword'>external_accounts[country]</span>=<span class='token string'>US</span> \
-d <span class='token keyword'>external_accounts[currency]</span>=<span class='token string'>usd</span> \
-d <span class='token keyword'>external_accounts[routing_number]</span>=<span class='token string'>110000000</span> \
-d <span class='token keyword'>external_accounts[account_number]</span>=<span class='token string'>000123456789</span> \
-d <span class='token keyword'>tos_acceptance[date]</span>=<span class='token string'>1464013708</span> \
-d <span class='token keyword'>tos_acceptance[ip]</span>="<span class='token string'>84.95.208.20</span>"
</code></pre>)
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
}
)
acct.external_accounts.object = 'bank_account'
acct.external_accounts.country = 'US'
acct.external_accounts.currency = 'usd'
acct.external_accounts.routing_number = '110000000'
acct.external_accounts.account_number = '000123456789'
acct.tos_acceptance.date = 1464013708
acct.tos_acceptance.ip = 84.95.208.20
acct.save
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')
acct.external_accounts.object = 'bank_account'
acct.external_accounts.country = 'US'
acct.external_accounts.currency = 'usd'
acct.external_accounts.routing_number = '110000000'
acct.external_accounts.account_number = '000123456789'
acct.tos_acceptance.date = 1464013708
acct.tos_acceptance.ip = 84.95.208.20
acct.save()
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" => 1464013708,
"ip" => "84.95.208.20"
)
));
$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 => 1464013708,
ip => 84.95.208.20
}
}, 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", false);
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", 1464013708);
tosParams.put("ip", 84.95.208.20);
accountParams.put("tos_acceptance", tosParams);
Account acct = Account.create(accountParams);
acctId = acct.id;
System.out.println(acctId);
2. First verification stage
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.
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.
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).
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.
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.
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.
<pre class="language-bash"><code class="language-bash">curl <span class='token string'>https://api.stripe.com/v1/charges</span> \
-u <span class='token string'>"$STRIPE_API_KEY:"</span> \
-d <span class='token keyword'>amount</span>=<span class='token string'>1000</span> \
-d <span class='token keyword'>currency</span>=<span class='token string'>usd</span> \
-d <span class='token keyword'>source[object]</span>=<span class='token string'>card</span> \
-d <span class='token keyword'>source[number]</span>=<span class='token string'>4000000000004202</span> \
-d <span class='token keyword'>source[exp_month]</span>=<span class='token string'>2</span> \
-d <span class='token keyword'>source[exp_year]</span>=<span class='token string'>2017</span> \
-d <span class='token keyword'>destination</span>="<span class='token string'>$ACCT_ID</span>"
</code></pre>
# Re-fetch the account to see what its status is.
<pre class="language-bash"><code class="language-bash">curl <span class='token string'>https://api.stripe.com/v1/accounts/$ACCT_ID</span> \
-u <span class='token string'>"$STRIPE_API_KEY:"</span>
</code></pre>
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));
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.
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).