Nurture TechnologiesNurture Tech
Integrations13 min read·July 24, 2026

Stripe Connect Verification Failed for Marketplace Users

Stripe ConnectNode.jsTypeScriptNext.js

Users completing your marketplace onboarding hit a verification wall Stripe rejects their identity documents or the connected account silently enters a restricted state. Here is the exact diagnosis path we followed.

Problem Summary

In Stripe Connect marketplaces, connected accounts (your sellers or service providers) must pass identity verification before payouts are enabled. When verification fails, the account enters a restricted state and payouts are blocked but Stripe does not send this information to users in a user-friendly way. Your platform must read the account.updated webhook, parse requirements.currently_due and requirements.errors, and surface actionable next steps to the account holder.

Symptoms

  • account.updated webhook fires with requirements.currently_due containing 'individual.verification.document'
  • Connected account payouts_enabled is false even after the user completed onboarding
  • Stripe Dashboard shows account in 'Restricted' or 'Restricted Soon' state
  • account.requirements.errors array contains verification error codes like 'verification_document_id_number_mismatch'
  • Users report completing identity upload but receiving no confirmation
  • AccountLink URLs expire before the user completes the flow
  • charges_enabled is true but payouts_enabled is false charges accepted but funds trapped
  • requirements.disabled_reason is 'requirements.past_due' and users cannot find what to fix

Business Impact

A marketplace where sellers cannot receive payouts is a marketplace that loses sellers. If your onboarding funnel drops users at the Stripe verification step without clear guidance, you lose the acquisition cost for every user who abandons. Worse, if verification fails silently after initial approval, funds accumulate in connected accounts that cannot be paid out, creating support burden and potential legal exposure.

Root Cause

Most Connect verification failures stem from three sources: (1) the platform does not listen to account.updated events and therefore never re-prompts users when Stripe requests additional information, (2) the AccountLink is generated once and cached but AccountLinks expire after 5 minutes, (3) the platform uses Express onboarding but passes insufficient pre-fill data, forcing users to enter information that could have been pre-populated.

AccountLinks expire after a single use or after 5 minutes, whichever comes first. Never store or reuse an AccountLink URL. Always generate a fresh URL when the user clicks the onboarding button.

Diagnosis

Step 1 Read the Account Object Directly

Retrieve the connected account object and inspect the requirements fields to understand exactly what Stripe is waiting for.

scripts/check-connect-account.ts
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2024-06-20",
});

async function diagnoseAccount(accountId: string) {
  const account = await stripe.accounts.retrieve(accountId);

  console.log("charges_enabled:", account.charges_enabled);
  console.log("payouts_enabled:", account.payouts_enabled);
  console.log("disabled_reason:", account.requirements?.disabled_reason);
  console.log(
    "currently_due:",
    JSON.stringify(account.requirements?.currently_due, null, 2)
  );
  console.log(
    "errors:",
    JSON.stringify(account.requirements?.errors, null, 2)
  );
  console.log(
    "eventually_due:",
    JSON.stringify(account.requirements?.eventually_due, null, 2)
  );
}

diagnoseAccount("acct_1ABC2DEF3GHI4JKL");

Step 2 Handle account.updated Webhook

The account.updated event fires whenever Stripe changes the connected account's requirements. Listen to this event and update your internal state, then notify the user if action is required.

src/webhooks/connect-handler.ts
import Stripe from "stripe";

type AccountRequirements = Stripe.Account.Requirements;

function isActionRequired(requirements: AccountRequirements | null): boolean {
  if (!requirements) return false;
  return (
    (requirements.currently_due?.length ?? 0) > 0 ||
    (requirements.past_due?.length ?? 0) > 0
  );
}

export async function handleAccountUpdated(event: Stripe.Event) {
  const account = event.data.object as Stripe.Account;

  await db.connectedAccounts.update({
    where: { stripeAccountId: account.id },
    data: {
      chargesEnabled: account.charges_enabled,
      payoutsEnabled: account.payouts_enabled,
      requirementsDisabledReason: account.requirements?.disabled_reason ?? null,
      requirementsCurrentlyDue: account.requirements?.currently_due ?? [],
      requirementsErrors: account.requirements?.errors ?? [],
      verificationStatus: account.payouts_enabled ? "verified" : "pending",
    },
  });

  if (isActionRequired(account.requirements ?? null)) {
    await notifyUserOfVerificationRequired(account.id);
  }
}

Never cache AccountLink URLs. Generate a new one each time the user clicks the verification button. Include refresh_url to handle expired links gracefully.

src/api/connect-onboarding.ts
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2024-06-20",
});

export async function createOnboardingLink(
  stripeAccountId: string,
  userId: string
): Promise<string> {
  const accountLink = await stripe.accountLinks.create({
    account: stripeAccountId,
    refresh_url: `${process.env.APP_URL}/onboarding/refresh?userId=${userId}`,
    return_url: `${process.env.APP_URL}/onboarding/complete?userId=${userId}`,
    type: "account_onboarding",
  });

  // Log for debugging  AccountLinks expire quickly
  console.log(
    `[Connect] AccountLink created for ${stripeAccountId}, expires: ${new Date(accountLink.expires_at * 1000).toISOString()}`
  );

  return accountLink.url;
}

// Handle the refresh_url route  user's session expired during onboarding
export async function handleOnboardingRefresh(userId: string): Promise<string> {
  const user = await db.users.findUnique({ where: { id: userId } });
  if (!user?.stripeAccountId) throw new Error("No connected account found");

  // Generate a fresh link  do NOT reuse the expired one
  return createOnboardingLink(user.stripeAccountId, userId);
}

Step 4 Parse Verification Error Codes

Stripe returns structured error codes in requirements.errors[].code. Map these to user-friendly messages rather than showing the raw Stripe error string.

src/utils/stripe-error-messages.ts
const VERIFICATION_ERROR_MESSAGES: Record<string, string> = {
  verification_document_id_number_mismatch:
    "The ID number on your document does not match our records. Please upload a document where the number is clearly visible.",
  verification_document_name_mismatch:
    "The name on your document does not match the name you provided. Ensure both match exactly.",
  verification_document_dob_mismatch:
    "Your date of birth does not match the document. Check that the correct document was uploaded.",
  verification_document_expired:
    "Your identity document has expired. Please upload a current, valid document.",
  verification_document_not_readable:
    "Your document was not readable. Please upload a clear, high-resolution photo with all four corners visible.",
  verification_document_address_mismatch:
    "The address on your document does not match the address you provided.",
  verification_failed_other:
    "Verification could not be completed. Please contact support for assistance.",
};

export function getVerificationErrorMessage(code: string): string {
  return (
    VERIFICATION_ERROR_MESSAGES[code] ??
    "An unexpected verification issue occurred. Please try again or contact support."
  );
}

Step 5 Pre-fill Account Data to Reduce Drop-off

If you already collected user information during your own registration flow, pass it to Stripe when creating the connected account. This reduces the friction of the Stripe onboarding form and lowers the likelihood of data mismatches causing verification failures.

src/api/create-connect-account.ts
import Stripe from "stripe";

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
  apiVersion: "2024-06-20",
});

export async function createConnectedAccount(user: {
  email: string;
  firstName: string;
  lastName: string;
  country: string;
}): Promise<string> {
  const account = await stripe.accounts.create({
    type: "express",
    country: user.country,
    email: user.email,
    capabilities: {
      card_payments: { requested: true },
      transfers: { requested: true },
    },
    business_type: "individual",
    individual: {
      email: user.email,
      first_name: user.firstName,
      last_name: user.lastName,
    },
    settings: {
      payouts: {
        schedule: {
          interval: "weekly",
          weekly_anchor: "friday",
        },
      },
    },
  });

  return account.id;
}

Production Failure Scenarios

Scenario 1 User Completes Onboarding but Account Stays Restricted

User clicks through the Stripe Express onboarding flow and lands on your return_url. The platform marks them as 'verified' based on the redirect. However, Stripe may require additional verification documents after initial submission especially for higher-risk categories. The platform must rely on account.updated events, not the return_url redirect, to determine actual verification status.

Scenario 2 Verification Works in Test Mode, Fails in Live Mode

Stripe test mode accepts any document and skips real KYC checks. When you go live, actual identity verification runs and finds mismatches that did not exist in test data. Always test the live onboarding flow before launch using a real identity document in a live-mode test account.

Scenario 3 Country-Specific Requirements Not Handled

Stripe requirements vary significantly by country. A US account requires SSN. A UK account requires a National Insurance Number or passport. An Indian account requires a PAN card. If your onboarding UI assumes a single country's requirements, international users will hit verification failures with no actionable guidance.

Prevention Checklist

  • Always subscribe to account.updated events for all connected accounts
  • Never cache AccountLink URLs generate a fresh one per user session
  • Always implement the refresh_url endpoint to recover from expired onboarding sessions
  • Read requirements.currently_due not charges_enabled to determine if action is needed
  • Map Stripe error codes to human-readable messages before displaying to users
  • Pre-fill connected account data with information you already collected
  • Test onboarding in live mode before launch using Stripe's test identity documents
  • Set up Dashboard alerts for accounts entering 'Restricted' state
  • Store requirements.errors in your database to enable support team troubleshooting
  • Implement proactive email notifications when account enters restricted_soon state

Resolution Summary

Fix Connect verification issues by (1) always listening to account.updated and updating your internal account state, (2) generating fresh AccountLinks on every user request instead of caching them, (3) reading requirements.currently_due and requirements.errors to display actionable guidance, and (4) pre-filling as much account data as possible during creation to reduce verification failure rates.

Lessons Learned

  • The return_url redirect is not a reliable signal of verification success always use account.updated webhooks
  • AccountLinks are single-use and expire in 5 minutes; build refresh flows from day one
  • Stripe requirements differ by country, business type, and transaction volume thresholds
  • Pre-filling account creation data reduces mismatch errors and improves onboarding completion rates
  • requirements.errors gives you specific, actionable error codes always surface these to users and support teams

Conclusion

Stripe Connect verification is not a one-time event it is an ongoing relationship between your platform and each connected account. Build your systems to respond dynamically to account.updated events, generate AccountLinks on demand, and translate Stripe's error codes into language your users can act on. The platforms that do this well have measurably better seller onboarding completion rates.

Nurture Technologies

NEED HELP WITH YOUR STACK?

Nurture Technologies builds and maintains production-quality software for startups and businesses. If engineering problems are slowing you down, our team can help.

Talk to our team →
FAQ

FREQUENTLY ASKED QUESTIONS

What is the difference between charges_enabled and payouts_enabled?+

charges_enabled means the connected account can accept payments on your platform. payouts_enabled means funds can be transferred from the Stripe balance to their bank account. Verification failures typically block payouts_enabled while leaving charges_enabled true meaning you can collect money but the seller cannot receive it.

Why does account.updated fire but requirements stay the same?+

account.updated fires for any change to the account, not just requirement changes. Check event.data.previous_attributes to see what actually changed. If requirements did not change, look at other fields like payouts_enabled or settings.

How long does Stripe identity verification take?+

Stripe's automated verification is typically instant. If Stripe's automated system cannot verify the document, it escalates to manual review which can take 1–3 business days. The account.updated event fires with the final decision.

Can I bypass Stripe's identity verification for trusted users?+

No. KYC (Know Your Customer) requirements are mandated by financial regulators and Stripe's banking partners. There is no way to bypass them. You can simplify the experience by pre-filling data, but the verification itself is mandatory for payouts.

What does disabled_reason: 'requirements.past_due' mean?+

It means Stripe previously requested information that was not provided within the deadline, and the account has been restricted as a result. The user must complete the items in requirements.past_due to restore full functionality.

How do I test Connect onboarding in Stripe test mode?+

Use Stripe's test mode with a test secret key. Stripe test mode skips real KYC and accepts any document. For testing specific verification outcomes, use Stripe's test identity verification numbers documented at stripe.com/docs/connect/testing.

Can a connected account be verified on one capability but not another?+

Yes. Capabilities (card_payments, transfers, etc.) each have independent requirement sets. An account can have card_payments.status = 'active' while transfers.status = 'inactive' because a different requirement is missing for transfers.

What happens to funds in a restricted connected account?+

Funds accumulate in the connected account's Stripe balance but cannot be paid out. They remain there until the restriction is resolved. Stripe does not automatically return them to customers.

Should I use Express or Custom Connect accounts?+

Express is strongly recommended for most marketplaces. Stripe hosts the onboarding UI and handles KYC, significantly reducing your compliance burden. Custom accounts give you full UI control but require you to build all onboarding flows and accept more compliance responsibility.

How do I notify users when their account enters a restricted state?+

Subscribe to account.updated webhook events. When requirements.currently_due is non-empty or requirements.disabled_reason is set, trigger an email or in-app notification with a fresh AccountLink URL to resume onboarding.