Creating your own marketplace with Stripe Connect & PHP— Like Shopify or Uber.

In this post, I will explain how to create your own marketplace. This means you can create charges like usual with Stripe, and you can also have your users setup a Stripe account on your website via their API.

Confused? Hopefully this example helps make it more clear. I’m the dev team behind of Shopify.com and I want my users to be able to create a store in my web application (on shopify.com) and they should be able to charge their customers too. You don’t want to send your users away to setup their payment information because it creates a bigger drop off rate and you’ll have to deal with the payment transfers.

Marketplaces and platforms use Stripe Connect to accept money and pay out to third parties. Connect provides a complete set of building blocks to support virtually any business model, including on-demand businesses, e‑commerce, crowdfunding, and travel and events. (Taken from the Stripe website).

I will be only covering Managed/Custom Stripe Connect accounts, meaning there is no OAuth with Stripe. All aspects of the account is managed on your web application via the Stripe API.

Setup

This tutorial is all done in PHP. I will release future articles for doing this in Python, and Node.js. I’m gonna run it all locally with MAMP (Mac, Apache, MySQL, PHP) on my mac. If you are using a Windows look at WAMP or Linux look at LAMP. You will also need to have Composer installed.

Start up your local server and navigate to your working directory on the terminal. Install the needed PHP packages with:

composer require stripe/stripe-php

This will download the Stripe PHP library. It will create a composer.json and composer.lock file.

Screenshot of MAMP running and before running composer install.

Assumptions

I’m going to make a few assumptions in this post. I’m going to assume you have some sort of log in system and each user has a primary key (Email, id number, etc.).

User Account Setup

The first step in actually programming is having your user enter their basic information to verify they can sell and process payments. This is depending on country and information entered. All potentially required information can be found under the Account object on their Stripe docs.

Create a new PHP page called setup.php that will render a form requesting the required (missing) information. $stripeAccountId is a column in your user table and begins null.

Here is step 1 of my code (complete code is at bottom):

<?php
// Required to import the Stripe library and others.
require_once('./vendor/autoload.php');
 // Two things grabbed from your account setup. Your user must be
// logged in. And then you do a look up in your user table for
// their corresponding stripeAccountId. If they haven't set it up
// yet, it will be null or ''.
$currentUserId = -1;
$stripeAccountId = '';
 // Put look up in user session here.
$currentUserId = 1;
// YOU_NEED_TO_ADD_CODE
 // Validate user logged in
if ($currentUserId == -1) {
die('Error: Invalid user log in.');
}
 // Put stripe account id from table look up here.
$stripeAccountId = '';
// YOU_NEED_TO_ADD_CODE
 // The structure for rendering and updating data is I'm going to
// load one needed item at a time. So for example if I need:
// sin_number, phone_number, name... I'll just load sin_number
// update that info then move on.
 // When there is no account id, load begin_setup as the value.
// In the action script, I'll know to create an account object.
 if ($stripeAccountId == '') {
// Case: Brand new user, never setup account.
  echo '<form action="./setup-action.php" method="POST">';
echo '<input type="text" hidden name="action_type" value="begin_setup" />';
echo '<button type="submit">Begin Account Setup</button>';
echo '</form>';

} else {
// Configure the library. You need to input your own test/live API
// Key. This can be found on your Stripe dashboard.
\Stripe\Stripe::setApiKey('YOUR_SECRET_API_KEY____sk_');
  // Retrieve object stuff will go here
 }
?>

You should see the following page:

Next, we will add the “action” code. This will handle the form we create in setup.php . Here is step 1 of the action code, the full code is at the bottom. I have commented most of it and this only handles the create account object. You do not need to save any of the user banking data because Stripe does this securely. You should not. They have a lot of smart people there, let them handle it. You just need to save the id in the result.

<?php
// Required to import the Stripe library and others.
require_once('./vendor/autoload.php');

// Two things grabbed from your account setup. Your user must be
// logged in. And then you do a look up in your user table for
// their corresponding stripeAccountId. If they haven't set it up
// yet, it will be null or ''.
$currentUserId = -1;
$stripeAccountId = '';
$currentUserEmail = '';
 // Put look up in user session here.
$currentUserId = 1;
// YOU_NEED_TO_ADD_CODE
 // Validate user logged in
if ($currentUserId == -1) {
die('Error: Invalid user log in.');
}
 // Put stripe account id from table look up here.
$stripeAccountId = '';
// YOU_NEED_TO_ADD_CODE
 // You also need to lookup the user email at the same time.
$currentUserEmail = 'test1@gmail.com';
// YOU_NEED_TO_ADD_CODE
 // Get the action type from the form submission.
$actionType = $_POST['action_type'];
 // More info about this on setup.php
\Stripe\Stripe::setApiKey('YOUR_SECRET_API_KEY____sk_');
 if ($actionType === 'begin_setup') {
// Create a new Stripe Connect Account object.
// For more info: https://stripe.com/docs/api#create_account
$result = \Stripe\Account::create(array(
"type" => "custom",
"country" => "US",
"email" => $currentUserEmail,
));
  // About these parameters, you can support more countries but
// you will have to limit your users. You need the proper
// country code and different type of information is required.
// Personally, I had issues with opening it up too much b/c
// a field may be called "Tax Id" which is called different
// things in different countries. In Canada, it's your SIN
// but when the label said Tax Id there was a high drop off
// rate because they didn't know what that was. We know it as
// a SIN number.
  // Result will contain a response from the Stripe API call.
// You want to use this information.
  // Both these functions will print out a tone of data returned.
// print_r($result);
// var_dump($result);
  // What you want is the stripeAccountId whic is accessed
// with $result->id or $result['id']
  // It will give you a string like this acct_112314324ANOSDNAID
// And YOU NEED TO SAVE IT WITH YOUR USER INFO. This is what
// we reference on setup.php this is now their stripeAccountId.
} else {
die('Invalid action type.');
}
?>

And you have created your first account object! But you still need a lot of information. If you open your dashboard, you can see this all in a nice UI or you can access via API.

The next step is programming setup.php to check for the missing/needed information since there is an account id.

We are going to retrieve the account object based on the id and look at the required elements. You can see in the returned object (based on the docs) that there is a fields needed array ( $stripeObj->verfication->fields_needed ).

To keep this post reasonable in length all the code below goes where the comment is Retrieve object stuff goes here .

<?php
... other code
  $stripeAccountObj = \Stripe\Account::retrieve($stripeAccountId);
  # So if fields needed is empty, you are good.
if (count($stripeAccountObj->verification->fields_needed) == 0) {
die('You are all setup!');
} else {
# Other wise load element one.
   # Following the same structure as above:
$neededCode = $stripeAccountObj->verification->fields_needed[0];
   echo '<form action="./setup-action.php" method="POST">';
echo '<input type="text" hidden name="action_type" value="' . $neededCode . '" />';
# I recommend customizing this a little.
echo '<label>' . $neededCode . '</label><br/>';
echo '<input type="text" name="value_textbox" required /><br/>';
echo '<button type="submit">Update</button>';
echo '</form>';
}
... other code
?>

And so with that you will see the first needed element. Now each field may need a specific format or a specific label. I am not including that in this posting, this is the bare minimum but I recommend controlling the labeling for the best user experience. The first field my account needs is the day from my date of birth.

We need to add the action type to the setup-action.php so it can update the account object. In order for this to be as dynamic as possible, I won’t program each potential field individually but update field based on the action type.

The code below is in setup-action.php and it’s replacing the else statement.

<?php
...
} else {
// Get the value
$value = $_POST['value_textbox'];
  if ($value == "") {
die('error: value cant be blank.');
}
  $stripeAccountObj = \Stripe\Account::retrieve($stripeAccountId);
  // Check for action type is actually needed
if (!in_array($actionType, $stripeAccountObj->verification->fields_needed)) {
die('Error: Not a required action type.');
}
  // Update the action type
$stripeAccountObj[$actionType] = $value;
$stripeAccountObj->save();
  // For more about this refer to the docs:
// https://stripe.com/docs/api#update_account
  echo 'Done';
}
...
?>

The only issue with the code above is the account object does not follow such a simple structure. For example, date of birth is nested in it’s own object so you have to create a function to handle this:

function updateNestedObject($stripeObj, $actionType, $value) {
// Use the explode to split by .
$arrayOfNestObjectKeys = explode('.', $actionType);
return updateChild($stripeObj, $arrayOfNestObjectKeys, $value);
}
function updateChild ($root, $listOfKeys, $value) {
if (count($listOfKeys) == 1) {
// Last element
$root[$listOfKeys[0]] = $value;
return $root;
} else {
// Other wise find the child
// Param 1: Grabs the child object
// Param 2: Takes the first elment off array
$child = $root[$listOfKeys[0]];
$remainingKeys = array_splice($listOfKeys, 1);
$root[$listOfKeys[0]] = updateChild ($child, $remainingKeys, $value);
return $root;
}
}

Put these two functions at the top of the file and add the following code instead of $stripeAccountObj[$actionType] = $value;:

  if (strpos($actionType, '.') !== false) {
// If the actionType has a . means it's nested.
// Ex. Date of birth is legal_entity.dob.day
// so to reference it you need to do
// $obj->legal_entity->dob->day
$stripeAccountObj = updateNestedObject($stripeAccountObj, $actionType, $value);
$stripeAccountObj->save();
} else {
// Update the action type
$stripeAccountObj[$actionType] = $value;
$stripeAccountObj->save();
}

And if you try again it still won’t work because of:

Missing required param: legal_entity[dob][month]

meaning you need to have the month and year at the same time. So we will have to change the setup-action.php to look for the following format YYYY-MM-DD if the action type is any of the date of birth ones.

The two functions are useless for date of birth but will be useful for future attributes.

We will need to create a special case. Replace the current action type if statement with the following:

  if (
$actionType === 'legal_entity.dob.day' ||
$actionType === 'legal_entity.dob.month' ||
$actionType === 'legal_entity.dob.year'
) {
// Special case:
$valueInPieces = explode('-', $value);
   // If invalid format, hence not 3 -
if (count($valueInPieces) != 3) {
// So not array of 3
die('Error: Invalid format for date, must be YYYY-MM-DD');
}
   // Otherwise good, save it!
$stripeAccountObj->legal_entity->dob->year = $valueInPieces[0];
$stripeAccountObj->legal_entity->dob->month = $valueInPieces[1];
$stripeAccountObj->legal_entity->dob->day = $valueInPieces[2];
$stripeAccountObj->save();
} else if (strpos($actionType, '.') !== false) {
// If the actionType has a . means it's nested.
// Ex. Date of birth is legal_entity.dob.day
// so to reference it you need to do
// $obj->legal_entity->dob->day
$stripeAccountObj = updateNestedObject($stripeAccountObj, $actionType, $value);
$stripeAccountObj->save();
} else {
// Update the action type
$stripeAccountObj[$actionType] = $value;
$stripeAccountObj->save();
}

If you are lost, the full code is at the bottom. Testing that out and it works!

When you reload setup.php , you should see a new field.

And just like that, you don’t need to add anymore code because those two functions added can handle the legal_entity.first_name case. I just entered a first name, reloaded setup then did last name, etc. legal_entity.type will be one of the required fields. This means is it a business or individual doing the charging. Different tax information will be needed in this case. Reference the Stripe docs for all of the fields because some have limited answers. For example with legal entity type, there are only two: individual or company. I want to change my setup.php to only load these options as a <select />.

   ... other code ...
# Following the same structure as above:
$neededCode = $stripeAccountObj->verification->fields_needed[0];
   if ($neededCode === 'legal_entity.type') {
    // Special case
$OPTIONS = ['individual' => 'Individual', 'company'=>'Company'];

echo '<form action="./setup-action.php" method="POST">';
echo '<input type="text" hidden name="action_type" value="' . $neededCode . '" />';
echo '<label>' . $neededCode . '</label><br/>';
     // Using a Select
echo '<select name="value_textbox">';
foreach ($OPTIONS as $key => $value) {
echo '<option value="' . $key . '">' . $value . '</option>';
}
echo '</select><br/>';
     echo '<button type="submit">Update</button>';
echo '</form>';
   } else {
echo '<form action="./setup-action.php" method="POST">';
echo '<input type="text" hidden name="action_type" value="' . $neededCode . '" />';
# I recommend customizing this a little.
echo '<label>' . $neededCode . '</label><br/>';
echo '<input type="text" name="value_textbox" required /><br/>';
echo '<button type="submit">Update</button>';
echo '</form>';
}
... other code ...

And it worked perfect:

You will find another special case with address. The address object expects the address, city, postal code/zip code, and country all at once. There will have to be custom UI for this field.

Add another special case to setup.php (same if statement as the legal_entity.types).

   } else if (
$neededCode === 'legal_entity.address.city' ||
$neededCode === 'legal_entity.address.country' ||
$neededCode === 'legal_entity.address.line1' ||
$neededCode === 'legal_entity.address.line2' ||
$neededCode === 'legal_entity.address.postal_code' ||
$neededCode === 'legal_entity.address.state'
) {
$COUNTRY_OPTIONS = ["CA" => "Canada", "US" => "United States"];
// Special case: address
echo '<form action="./setup-action.php" method="POST">';
echo '<input type="text" hidden name="action_type" value="address" />';
echo '<label>Address:</label><br/>';
echo '<input type="text" placeholder="123 Fake Street" name="line_textbox" required /><br/>';
echo '<input type="text" placeholder="Apartment 1" name="line2_textbox" /><br/>';
echo '<input type="text" placeholder="Toronto" name="city_textbox" required /><br/>';
echo '<input type="text" placeholder="Ontario" name="state_textbox" required /><br/>';
echo '<select name="country_textbox">';
foreach ($COUNTRY_OPTIONS as $key => $value) {
echo '<option value="' . $key . '">' . $value . '</option>';
}
echo '</select><br/>';
echo '<input type="text" placeholder="M8K 8L3" name="postal_textbox" required /><br/>';
echo '<button type="submit">Update</button>';
echo '</form>';

And it will look like:

You will then need to update the setup-action.php file to support this. I’m gonna include the second half of this file as it has gotten pretty big. All of this code goes into the else if and else of the if ($actionType == 'begin_setup') { check.

 } else if (
$actionType === 'legal_entity.address.city' ||
$actionType === 'legal_entity.address.country' ||
$actionType === 'legal_entity.address.line1' ||
$actionType === 'legal_entity.address.line2' ||
$actionType === 'legal_entity.address.postal_code' ||
$actionType === 'legal_entity.address.state' ||
$actionType === 'address'
) {
// Special case for address
$line = $_POST['line_textbox'];
$line2 = $_POST['line2_textbox'];
$city = $_POST['city_textbox'];
$state = $_POST['state_textbox'];
$country = $_POST['country_textbox'];
$postal = $_POST['postal_textbox'];
  // Check for all required values
if (
$line == '' ||
$city == '' ||
$state == '' ||
$country == '' ||
$postal == ''
) {
die('Error: Missing required address value.');
}
  // Got this error: Uncaught InvalidArgumentException: You cannot set 'line2'to an empty string. We interpret empty strings as NULL in requests. You may set obj->line2 = NULL to delete the property in
// So this handles it
if ($line2 == '') {
$line = null;
}
  $stripeAccountObj = \Stripe\Account::retrieve($stripeAccountId);
$stripeAccountObj->legal_entity->address->line1 = $line;
if ($line2 != "") {
$stripeAccountObj->legal_entity->address->line2 = $line2;
}
$stripeAccountObj->legal_entity->address->city = $city;
$stripeAccountObj->legal_entity->address->state = $state;
$stripeAccountObj->legal_entity->address->country = $country;
$stripeAccountObj->legal_entity->address->postal_code = $postal;
$stripeAccountObj->save();
echo 'Done';
 } else {
... other code
}

If you look in the MAMP/logs/php_error.log you can find the reason a 500 is being thrown. The reason why I bring this up is because I’m getting an error ( Address for business must match account country ). This being said the easiest fix is to remove the Canada option from the setup.php.

After making that change, address should work! Next thing asked for is legal_entity.ssn_last_4 (Last 4 digits of a SSN number). This isn’t a special case. Then you’ll be asked for legal_entity.personal_id_number.

Eventually you will run into the follow required field legal_entity.verification.document and there will have to be a special case, both on the front end for file upload and on the backend. I’m going to use the W3 Schools file upload for the needed frontend code and my Github Gist for the server side code (I’m going to remove the AWS S3 Part).

My setup.php will have this:

... other code
} else if ($neededCode == 'legal_entity.verification.document') {
// Special case:
echo '<form action="./setup-action.php" method="POST" enctype="multipart/form-data">';
echo '<input type="text" hidden name="action_type" value="' . $neededCode . '" />';
# I recommend customizing this a little.
echo '<label>' . $neededCode . '</label><br/>';
echo '<input type="file" name="fileToUpload" required /><br/>';
echo '<button type="submit">Update</button>';
echo '</form>';
} else {
... other code

Before getting started on the server side of the code, I should mention I am going to file the file upload guide given by Stripe. Based on the docs, we will grab the file from the form, upload it to the Stripe S3 bucket, and receive a response. There will be an id in the response and we will save that into our account object. The server code will be:

} else if ($actionType === 'legal_entity.verification.document') {
$file = $_FILES["fileToUpload"]['tmp_name'];
  $fp = fopen($file, 'r');
$fileResponse = \Stripe\FileUpload::create(array(
'purpose' => 'identity_document',
'file' => $fp
));
  $stripeAccountObj = \Stripe\Account::retrieve($stripeAccountId);
$stripeAccountObj->legal_entity->verification->document =
$fileResponse->id;
$stripeAccountObj->save();
  echo 'Done';
} else {
... other code

And works! You can upload any image for this. Stripe is trying to verify your customer so they are requesting a scan of a government issued id. But for testing, any image works. If this does not work, it’s most likely a permissions error.

Right away refreshing gives me tos_acceptance.date which means the date they accepted the Terms of Service. For the TOS, I recommend handling this right at the beginning. Putting a message like, “By clicking Begin Setup, you agree to our terms and services in addition to the Stripe Connected Account Agreement.” Stripe has a lot of information about this on their TOS website.

I’m going to update the initial form in setup.php:

 if ($stripeAccountId == '') {
// Case: Brand new user, never setup account.
  echo '<form action="./setup-action.php" method="POST">';
echo '<input type="text" hidden name="action_type" value="begin_setup" />';
echo '<label>By registering your account, you agree to our <a href="#Terms">Services Agreement</a> and the <a href="https://stripe.com/us/connect-account/legal">Stripe Connected Account Agreement</a>.</label>';
echo '<button type="submit">Begin Account Setup</button>';
echo '</form>';
} else {
... other code

and on the action side we will need to add a retrieve and update right after the create. I added:

if ($actionType === 'begin_setup') {
// Create a new Stripe Connect Account object.
// For more info: https://stripe.com/docs/api#create_account
$result = \Stripe\Account::create(array(
"type" => "custom",
"country" => "US",
"email" => $currentUserEmail,
));
$stripeAccountId = $result->id;
// Save stripe account id in db
  // echo 'stripeAccountId:' . $stripeAccountId;
  // Accept the TOS
$stripeAccountObj = \Stripe\Account::retrieve($stripeAccountId);
$stripeAccountObj->tos_acceptance->date = time();
$stripeAccountObj->tos_acceptance->ip = $_SERVER['REMOTE_ADDR'];
$stripeAccountObj->save();

You will have to clear the current user, you are testing with. They will never be able to accept the TOS because you have already created the id. Delete the stripeAccountId in the table and reload the setup page. You should see the begin setup button.

You will have to go through the whole verification process again but now Terms of Service are accept at the start. So date of birth, first name, last name, account type, address, ssn_last_4, personal_id_number (must be 9 digits),and verification document.

The first new field will be bank account. Bank account is a special case but not like the other special cases. It requires some Javascript and generating a token using the Stripe JS Library and passing that token to the backend.

If you look at the docs, they provide you with the information to set this up. You will have to add both JS and PHP code. Starting with the form (setup.php), you will add another case:

   } else if ($neededCode == 'bank_account') {
echo '<form id="bankingForm" method="POST">';
echo '<input type="text" hidden name="action_type" value="banking" />';
# I recommend customizing this a little.
echo '<label>' . $neededCode . '</label><br/>';
echo '<input type="text" id="routing_number" required /><br/>';
echo '<input type="text" id="account_number" required /><br/>';
echo '<input type="text" id="account_holder_name" required /><br/>';
     $ACCOUNT_TYPE_OPTIONS = ['individual' => 'Individual', 'business' => 'Business'];
echo '<select id="account_holder_type">';
foreach ($ACCOUNT_TYPE_OPTIONS as $key => $value) {
echo '<option value="' . $key . '">' . $value . '</option>';
}
echo '</select><br/>';
    echo '<button type="submit">Update</button>';
echo '</form>';
echo '<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>';
echo '<script src="https://js.stripe.com/v3/"></script>';
echo '<script>';
echo 'var stripe = Stripe(\'YOUR_PUBLISHABLE_API_KEY____pb_\', { stripeAccount: \'' . $stripeAccountId . '\' });';
echo '</script>';
echo '<script src="./bank-account.js"></script>';
} else {
... other code

An explanation of the code above, you have a form with the possible banking elements. And you have to dynamically add in the Stripe Account id. You’ll see the publishable API Key there, you will have to get this from the dashboard. Then you have to create a new JS file called bank-account.js . This will use the Stripe.js library and generate a token. You will then pass the token via an AJAX post request to the action script. This is why JQuery is now included, to do this AJAX request.

bank-account.js looks like:

$("#bankingForm").submit(function(event){
event.preventDefault();
console.log('submitBankAccount');
stripe.createToken('bank_account', {
country: 'US',
currency: 'usd',
routing_number: $("#routing_number").val(),
account_number: $("#account_number").val(),
account_holder_name: $("#account_holder_name").val(),
account_holder_type: $("#account_holder_type").val(),
}).then(function(result) {
// Handle result.error or result.token
console.log(result);
if (result.error) {
alert('Error');
console.log(result.error);
} else {
$.post(
'./setup-action.php',
{
token: result.token,
action_type: 'banking',
},
function (data) {
console.log('response');
console.log(data);
// If successful reload page
location.reload();
}
);
}
});
return false;
});

I recommend adding some checks for formatting and stuff but this will work. If the request is successful, it will refresh the page. Next you have to add the stripe-action.php code. This is similar because the token is done on the front-end and all you are passing is the whole object but you just need the id.

 } else if ($actionType === 'banking') {
$token = $_POST['token'];
if ($token == '') {
$response['success'] = false;
$response['message'] = 'No token';
echo json_encode($response);
die('');
}
  $stripeAccountObj = \Stripe\Account::retrieve($stripeAccountId);
$stripeAccountObj->external_accounts->create(array("external_account" => $token['id']));
$stripeAccountObj->save();
} else {
... other code ...

This will save the token. For testing, take a look at the Stripe Connect Testing page. It has all the test account numbers and such. For this, you want the United States accounts.

Submit the form and:

You are done! We have only tested the individual account and only for the United States but you will just have to add more special cases.

I’m going to include the full code but below it will be charging people.

setup.php

<?php
// Required to import the Stripe library and others.
require_once('./vendor/autoload.php');
// Two things grabbed from your account setup. Your user must be
// logged in. And then you do a look up in your user table for
// their corresponding stripeAccountId. If they haven't set it up
// yet, it will be null or ''.
$currentUserId = -1;
$stripeAccountId = '';
// Put look up in user session here.
$currentUserId = 1;
// YOU_NEED_TO_ADD_CODE
// Validate user logged in
if ($currentUserId == -1) {
die('Error: Invalid user log in.');
}
// Put stripe account id from table look up here.
$stripeAccountId = '';
// YOU_NEED_TO_ADD_CODE
// The structure for rendering and updating data is I'm going to
// load one needed item at a time. So for example if I need:
// sin_number, phone_number, name... I'll just load sin_number
// update that info then move on.
// When there is no account id, load begin_setup as the value.
// In the action script, I'll know to create an account object.
if ($stripeAccountId == '') {
// Case: Brand new user, never setup account.
echo '<form action="./setup-action.php" method="POST">';
echo '<input type="text" hidden name="action_type" value="begin_setup" />';
echo '<label>By registering your account, you agree to our <a href="#Terms">Services Agreement</a> and the <a href="https://stripe.com/us/connect-account/legal">Stripe Connected Account Agreement</a>.</label>';
echo '<button type="submit">Begin Account Setup</button>';
echo '</form>';

} else {
// Configure the library. You need to input your own test/live API Key.
// This can be found on your Stripe dashboard.
\Stripe\Stripe::setApiKey('YOUR_SECRET_API_KEY____sk_');
$stripeAccountObj = \Stripe\Account::retrieve($stripeAccountId);
# So if fields needed is empty, you are good.
if (count($stripeAccountObj->verification->fields_needed) == 0) {
die('You are all setup!');
} else {
# Other wise load element one.
# Following the same structure as above:
$neededCode = $stripeAccountObj->verification->fields_needed[0];
if ($neededCode === 'legal_entity.type') {
// Special case
$OPTIONS = ['individual' => 'Individual', 'company'=>'Company'];
echo '<form action="./setup-action.php" method="POST">';
echo '<input type="text" hidden name="action_type" value="' . $neededCode . '" />';
echo '<label>' . $neededCode . '</label><br/>';
// Using a Select
echo '<select name="value_textbox">';
foreach ($OPTIONS as $key => $value) {
echo '<option value="' . $key . '">' . $value . '</option>';
}
echo '</select><br/>';
echo '<button type="submit">Update</button>';
echo '</form>';
} else if (
$neededCode === 'legal_entity.address.city' ||
$neededCode === 'legal_entity.address.country' ||
$neededCode === 'legal_entity.address.line1' ||
$neededCode === 'legal_entity.address.line2' ||
$neededCode === 'legal_entity.address.postal_code' ||
$neededCode === 'legal_entity.address.state'
) {
$COUNTRY_OPTIONS = ["US" => "United States"];
// Special case: address
echo '<form action="./setup-action.php" method="POST">';
echo '<input type="text" hidden name="action_type" value="address" />';
echo '<label>Address:</label><br/>';
echo '<input type="text" placeholder="123 Fake Street" name="line_textbox" required /><br/>';
echo '<input type="text" placeholder="Apartment 1" name="line2_textbox" /><br/>';
echo '<input type="text" placeholder="Toronto" name="city_textbox" required /><br/>';
echo '<input type="text" placeholder="Ontario" name="state_textbox" required /><br/>';
echo '<select name="country_textbox">';
foreach ($COUNTRY_OPTIONS as $key => $value) {
echo '<option value="' . $key . '">' . $value . '</option>';
}
echo '</select><br/>';
echo '<input type="text" placeholder="M8K 8L3" name="postal_textbox" required /><br/>';
echo '<button type="submit">Update</button>';
echo '</form>';
} else if ($neededCode == 'legal_entity.verification.document') {
// Special case:
echo '<form action="./setup-action.php" method="POST" enctype="multipart/form-data">';
echo '<input type="text" hidden name="action_type" value="' . $neededCode . '" />';
# I recommend customizing this a little.
echo '<label>' . $neededCode . '</label><br/>';
echo '<input type="file" name="fileToUpload" required /><br/>';
echo '<button type="submit">Update</button>';
echo '</form>';
} else if ($neededCode == 'bank_account') {
echo '<form id="bankingForm" method="POST">';
echo '<input type="text" hidden name="action_type" value="banking" />';
# I recommend customizing this a little.
echo '<label>' . $neededCode . '</label><br/>';
echo '<input type="text" id="routing_number" required /><br/>';
echo '<input type="text" id="account_number" required /><br/>';
echo '<input type="text" id="account_holder_name" required /><br/>';
$ACCOUNT_TYPE_OPTIONS = ['individual' => 'Individual', 'business' => 'Business'];
echo '<select id="account_holder_type">';
foreach ($ACCOUNT_TYPE_OPTIONS as $key => $value) {
echo '<option value="' . $key . '">' . $value . '</option>';
}
echo '</select><br/>';
    echo '<button type="submit">Update</button>';
echo '</form>';
echo '<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>';
echo '<script src="https://js.stripe.com/v3/"></script>';
echo '<script>';
echo 'var stripe = Stripe(\'YOUR_PUBLISHABLE_API_KEY____pb_\', { stripeAccount: \'' . $stripeAccountId . '\' });';
echo '</script>';
echo '<script src="./bank-account.js"></script>';
} else {
echo '<form action="./setup-action.php" method="POST">';
echo '<input type="text" hidden name="action_type" value="' . $neededCode . '" />';
# I recommend customizing this a little.
echo '<label>' . $neededCode . '</label><br/>';
echo '<input type="text" name="value_textbox" required /><br/>';
echo '<button type="submit">Update</button>';
echo '</form>';
}
}
}
?>

setup-action.php

<?php
function updateNestedObject($stripeObj, $actionType, $value) {
// Use the explode to split by .
$arrayOfNestObjectKeys = explode('.', $actionType);
return updateChild($stripeObj, $arrayOfNestObjectKeys, $value);
}
function updateChild ($root, $listOfKeys, $value) {
if (count($listOfKeys) == 1) {
// Last element
$root[$listOfKeys[0]] = $value;
return $root;
} else {
// Other wise find the child
// Param 1: Grabs the child object
// Param 2: Takes the first elment off array
$child = $root[$listOfKeys[0]];
$remainingKeys = array_splice($listOfKeys, 1);
$root[$listOfKeys[0]] = updateChild ($child, $remainingKeys, $value);
return $root;
}
}
// Required to import the Stripe library and others.
require_once('./vendor/autoload.php');

// Two things grabbed from your account setup. Your user must be
// logged in. And then you do a look up in your user table for
// their corresponding stripeAccountId. If they haven't set it up
// yet, it will be null or ''.
$currentUserId = -1;
$stripeAccountId = '';
$currentUserEmail = '';
// Put look up in user session here.
$currentUserId = 1;
// YOU_NEED_TO_ADD_CODE
 // Validate user logged in
if ($currentUserId == -1) {
die('Error: Invalid user log in.');
}
// Put stripe account id from table look up here.
$stripeAccountId = '';
// YOU_NEED_TO_ADD_CODE
// You also need to lookup the user email at the same time.
$currentUserEmail = 'test1@gmail.com';
// YOU_NEED_TO_ADD_CODE
// Get the action type from the form submission.
$actionType = $_POST['action_type'];
 // More info about this on setup.php
\Stripe\Stripe::setApiKey('YOUR_SECRET_API_KEY____sk_');
if ($actionType === 'begin_setup') {
// Create a new Stripe Connect Account object.
// For more info: https://stripe.com/docs/api#create_account
$result = \Stripe\Account::create(array(
"type" => "custom",
"country" => "US",
"email" => $currentUserEmail,
));
// About these parameters, you can support more countries but
// you will have to limit your users. You need the proper
// country code and different type of information is required.
// Personally, I had issues with opening it up too much b/c
// a field may be called "Tax Id" which is called different
// things in different countries. In Canada, it's your SIN
// but when the label said Tax Id there was a high drop off
// rate because they didn't know what that was. We know it as
// a SIN number.
// Result will contain a response from the Stripe API call.
// You want to use this information.
// Both these functions will print out a tone of data returned.
// print_r($result);
// var_dump($result);
// What you want is the stripeAccountId whic is accessed
// with $result->id or $result['id']
// It will give you a string like this acct_112314324ANOSDNAID
// And YOU NEED TO SAVE IT WITH YOUR USER INFO. This is what
// we reference on setup.php this is now their stripeAccountId.
$stripeAccountId = $result->id;
echo 'stripeAccountId:' . $stripeAccountId;
// Accept the TOS
$stripeAccountObj = \Stripe\Account::retrieve($stripeAccountId);
$stripeAccountObj->tos_acceptance->date = time();
$stripeAccountObj->tos_acceptance->ip = $_SERVER['REMOTE_ADDR'];
$stripeAccountObj->save();
} else if (
$actionType === 'legal_entity.address.city' ||
$actionType === 'legal_entity.address.country' ||
$actionType === 'legal_entity.address.line1' ||
$actionType === 'legal_entity.address.line2' ||
$actionType === 'legal_entity.address.postal_code' ||
$actionType === 'legal_entity.address.state' ||
$actionType === 'address'
) {
// Special case for address
$line = $_POST['line_textbox'];
$line2 = $_POST['line2_textbox'];
$city = $_POST['city_textbox'];
$state = $_POST['state_textbox'];
$country = $_POST['country_textbox'];
$postal = $_POST['postal_textbox'];
// Check for all required values
if (
$line == '' ||
$city == '' ||
$state == '' ||
$country == '' ||
$postal == ''
) {
die('Error: Missing required address value.');
}
$stripeAccountObj = \Stripe\Account::retrieve($stripeAccountId);
$stripeAccountObj->legal_entity->address->line1 = $line;
if ($line2 != "") {
$stripeAccountObj->legal_entity->address->line2 = $line2;
}
$stripeAccountObj->legal_entity->address->city = $city;
$stripeAccountObj->legal_entity->address->state = $state;
$stripeAccountObj->legal_entity->address->country = $country;
$stripeAccountObj->legal_entity->address->postal_code = $postal;
$stripeAccountObj->save();
echo 'Done';
} else if ($actionType === 'legal_entity.verification.document') {
$file = $_FILES["fileToUpload"]['tmp_name'];
$fp = fopen($file, 'r');
$fileResponse = \Stripe\FileUpload::create(array(
'purpose' => 'identity_document',
'file' => $fp
));
$stripeAccountObj = \Stripe\Account::retrieve($stripeAccountId);
$stripeAccountObj->legal_entity->verification->document = $fileResponse->id;
$stripeAccountObj->save();
echo 'Done';
} else if ($actionType === 'banking') {
$token = $_POST['token'];
if ($token == '') {
$response['success'] = false;
$response['message'] = 'No token';
echo json_encode($response);
die('');
}
$stripeAccountObj = \Stripe\Account::retrieve($stripeAccountId);
$stripeAccountObj->external_accounts->create(array("external_account" => $token['id']));
$stripeAccountObj->save();
} else {
// Get the value
$value = $_POST['value_textbox'];
if ($value == "") {
die('error: value cant be blank.');
}
$stripeAccountObj = \Stripe\Account::retrieve($stripeAccountId);
// Check for action type is actually needed
if (!in_array($actionType, $stripeAccountObj->verification->fields_needed)) {
die('Error: Not a required action type.');
}
if (
$actionType === 'legal_entity.dob.day' ||
$actionType === 'legal_entity.dob.month' ||
$actionType === 'legal_entity.dob.year'
) {
// Special case:
$valueInPieces = explode('-', $value);
// If invalid format, hence not 3 -
if (count($valueInPieces) != 3) {
// So not array of 3
die('Error: Invalid format for date, must be YYYY-MM-DD');
}
// Otherwise good, save it!
$stripeAccountObj->legal_entity->dob->year = $valueInPieces[0];
$stripeAccountObj->legal_entity->dob->month = $valueInPieces[1];
$stripeAccountObj->legal_entity->dob->day = $valueInPieces[2];
$stripeAccountObj->save();
} else if (strpos($actionType, '.') !== false) {
// If the actionType has a . means it's nested.
// Ex. Date of birth is legal_entity.dob.day
// so to reference it you need to do
// $obj->legal_entity->dob->day
$stripeAccountObj = updateNestedObject($stripeAccountObj, $actionType, $value);
$stripeAccountObj->save();
} else {
// Update the action type
$stripeAccountObj[$actionType] = $value;
$stripeAccountObj->save();
}
// For more about this refer to the docs:
// https://stripe.com/docs/api#update_account
  echo 'Done';
}
?>

bank-account.js

$("#bankingForm").submit(function(event){
event.preventDefault();
console.log('submitBankAccount');
stripe.createToken('bank_account', {
country: 'US',
currency: 'usd',
routing_number: $("#routing_number").val(),
account_number: $("#account_number").val(),
account_holder_name: $("#account_holder_name").val(),
account_holder_type: $("#account_holder_type").val(),
}).then(function(result) {
// Handle result.error or result.token
console.log(result);
if (result.error) {
alert('Error');
console.log(result.error);
} else {
$.post(
'./setup-action.php',
{
token: result.token,
action_type: 'banking',
},
function (data) {
console.log('response');
console.log(data);
// If successful reload page
location.reload();
}
);
}
});
return false;
});

Creating a Charge

Creating a charge is pretty simple, all you need is the Stripe account id. Similar to a regular charge, you just pass in the id :

\Stripe\Stripe::setApiKey("YOUR_SECRET_STRIPE_KEY");

$charge = \Stripe\Charge::create(array(
"amount" => 1000,
"currency" => "usd",
"source" => "tok_visa",
), array("stripe_account" => $stripeAccountId));

The code above assumes you have a variable called stripeAccountId .