# How to create an Express account with Stripe Connect

- Tool: Stripe
- Difficulty: Advanced
- Time required: 20 minutes
- Compatibility: Stripe API v2024-12+, Node.js 18+
- Last updated: March 2026

## TL;DR

Stripe Connect Express accounts let you onboard sellers with minimal code using Stripe-hosted forms. You create the account via the API, generate an Account Link for onboarding, and redirect the seller. Stripe handles identity verification, bank account collection, and compliance. This guide covers the full flow from account creation to confirming the seller is ready to accept payments.

## Express Account Onboarding with Stripe Connect

Express accounts are the most popular Connect account type because Stripe handles the onboarding UI, identity verification, and compliance requirements. Your platform creates the account, generates an onboarding link, and redirects the seller. After onboarding, the seller gets access to a simplified Express Dashboard to view payouts and manage their account.

## Before you start

- A Stripe account with Connect enabled
- Node.js 18 or later installed
- Stripe Node.js SDK: npm install stripe
- Express.js for the server: npm install express

## Step-by-step guide

### 1. Create the Express connected account

Create a new Express account for your seller. You can pre-fill information like email and country to streamline onboarding.

```
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);

async function createExpressAccount(email, country) {
  const account = await stripe.accounts.create({
    type: 'express',
    country: country || 'US',
    email: email,
    capabilities: {
      card_payments: { requested: true },
      transfers: { requested: true },
    },
    business_type: 'individual',
    metadata: {
      platform_user_id: 'user_123', // Link to your internal user
    },
  });
  console.log('Account created:', account.id);
  return account;
}

createExpressAccount('seller@example.com', 'US');
```

**Expected result:** A new Express connected account is created with an ID like acct_1N...

### 2. Generate the onboarding Account Link

Account Links are short-lived URLs that redirect the seller to Stripe's hosted onboarding. Generate a new one each time the seller starts or resumes onboarding.

```
async function createAccountLink(accountId) {
  const accountLink = await stripe.accountLinks.create({
    account: accountId,
    refresh_url: 'https://yourplatform.com/onboarding/refresh',
    return_url: 'https://yourplatform.com/onboarding/complete',
    type: 'account_onboarding',
  });
  console.log('Redirect seller to:', accountLink.url);
  return accountLink;
}

createAccountLink('acct_1N...');
```

**Expected result:** A URL is returned. Redirecting the seller to this URL shows Stripe's onboarding form.

### 3. Handle the return URL

When the seller completes onboarding (or leaves midway), Stripe redirects them to your return_url. Check the account status to determine if onboarding is complete.

```
const express = require('express');
const app = express();

app.get('/onboarding/complete', async (req, res) => {
  const accountId = req.query.account_id; // You'd store this in session
  const account = await stripe.accounts.retrieve(accountId);

  if (account.charges_enabled && account.payouts_enabled) {
    res.send('Onboarding complete! You can now accept payments.');
  } else {
    // Seller left early or requirements still pending
    const link = await stripe.accountLinks.create({
      account: accountId,
      refresh_url: 'https://yourplatform.com/onboarding/refresh',
      return_url: 'https://yourplatform.com/onboarding/complete',
      type: 'account_onboarding',
    });
    res.redirect(link.url);
  }
});

app.get('/onboarding/refresh', async (req, res) => {
  // Account Link expired, generate a new one
  const accountId = req.query.account_id;
  const link = await stripe.accountLinks.create({
    account: accountId,
    refresh_url: 'https://yourplatform.com/onboarding/refresh',
    return_url: 'https://yourplatform.com/onboarding/complete',
    type: 'account_onboarding',
  });
  res.redirect(link.url);
});
```

**Expected result:** Sellers who complete onboarding see a success message. Sellers who leave early are redirected back to continue.

### 4. Listen for account.updated webhooks

Onboarding completion is asynchronous — Stripe verifies documents in the background. Use webhooks to get notified when the account is fully enabled.

```
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body,
      sig,
      process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'account.updated') {
    const account = event.data.object;
    if (account.charges_enabled && account.payouts_enabled) {
      console.log(`Account ${account.id} fully onboarded!`);
      // Update your database: mark seller as active
    }
  }

  res.json({ received: true });
});
```

**Expected result:** Your server logs when a connected account becomes fully onboarded and ready to accept payments.

## Complete code example

File: `express-account-onboarding.js`

```javascript
const stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
const express = require('express');
const app = express();

// Store account IDs in your database — this is a simplified in-memory map
const sellerAccounts = new Map();

app.use(express.json());

app.post('/api/onboard-seller', async (req, res) => {
  try {
    const account = await stripe.accounts.create({
      type: 'express',
      country: req.body.country || 'US',
      email: req.body.email,
      capabilities: {
        card_payments: { requested: true },
        transfers: { requested: true },
      },
      business_type: 'individual',
    });

    sellerAccounts.set(req.body.email, account.id);

    const accountLink = await stripe.accountLinks.create({
      account: account.id,
      refresh_url: `${req.headers.origin}/onboarding/refresh?acct=${account.id}`,
      return_url: `${req.headers.origin}/onboarding/complete?acct=${account.id}`,
      type: 'account_onboarding',
    });

    res.json({ accountId: account.id, url: accountLink.url });
  } catch (err) {
    res.status(400).json({ error: err.message });
  }
});

app.get('/onboarding/complete', async (req, res) => {
  const account = await stripe.accounts.retrieve(req.query.acct);
  if (account.charges_enabled) {
    res.send('Onboarding complete! You are ready to accept payments.');
  } else {
    const link = await stripe.accountLinks.create({
      account: req.query.acct,
      refresh_url: `https://yourplatform.com/onboarding/refresh?acct=${req.query.acct}`,
      return_url: `https://yourplatform.com/onboarding/complete?acct=${req.query.acct}`,
      type: 'account_onboarding',
    });
    res.redirect(link.url);
  }
});

app.get('/onboarding/refresh', async (req, res) => {
  const link = await stripe.accountLinks.create({
    account: req.query.acct,
    refresh_url: `https://yourplatform.com/onboarding/refresh?acct=${req.query.acct}`,
    return_url: `https://yourplatform.com/onboarding/complete?acct=${req.query.acct}`,
    type: 'account_onboarding',
  });
  res.redirect(link.url);
});

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body, sig, process.env.STRIPE_WEBHOOK_SECRET
    );
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  if (event.type === 'account.updated') {
    const acct = event.data.object;
    console.log(`Account ${acct.id}: charges=${acct.charges_enabled} payouts=${acct.payouts_enabled}`);
  }
  res.json({ received: true });
});

app.listen(3000, () => console.log('Server on port 3000'));
```

## Common mistakes

- **Caching Account Link URLs instead of generating new ones each time** — undefined Fix: Account Links expire quickly. Always create a fresh link when the seller clicks your onboarding button.
- **Assuming the seller is fully onboarded when they return to your return_url** — undefined Fix: The return_url only means the seller left the onboarding flow. Always check charges_enabled and payouts_enabled to confirm.
- **Not handling the refresh_url** — undefined Fix: If the Account Link expires, Stripe redirects to refresh_url. Generate a new link and redirect the seller back to continue.
- **Forgetting to request capabilities** — undefined Fix: Without card_payments and transfers capabilities, the account cannot process payments or receive funds.

## Best practices

- Store the connected account ID in your database immediately after creation, linked to your platform user
- Always generate fresh Account Links — never cache or reuse them
- Use webhooks (account.updated) as the source of truth for onboarding status, not the return URL
- Pre-fill email and country to reduce onboarding friction
- Add metadata to connected accounts to link them to your internal user IDs
- Test the full onboarding flow in test mode before enabling live mode
- Show a clear onboarding progress indicator in your UI

## Frequently asked questions

### How long does Express account onboarding take?

The seller can complete the onboarding form in 5-10 minutes. Stripe's verification usually takes seconds for automated checks, but document review can take 1-3 business days.

### Can I customize the Express onboarding form?

You can customize the brand color, icon, and name shown on the onboarding form via Dashboard → Connect → Settings → Branding. The form layout itself is controlled by Stripe.

### What happens if a seller abandons onboarding midway?

The account remains in a partially onboarded state. You can generate a new Account Link to let them resume. Their progress is saved.

### Can I pre-fill more than email and country?

Yes. You can pre-fill business_type, individual details (name, DOB, address), and business_profile (URL, MCC) when creating the account to skip those steps in onboarding.

### Is there a cost for Express accounts?

Creating Express accounts is free. Stripe charges standard processing fees on transactions plus an additional fee per payout to connected accounts (varies by country).

---

Source: https://www.rapidevelopers.com/stripe-guide/how-to-create-an-express-account-with-stripe-connect
© RapidDev — https://www.rapidevelopers.com/stripe-guide/how-to-create-an-express-account-with-stripe-connect
