BIO.RE
Creator

Initiate Stripe Connect

Create a Stripe Connect Express account and return a hosted onboarding URL. KYC must be APPROVED first. Idempotent — repeated calls return a fresh link for the same account.

POST /api/v1/creators/stripe-connect/initiate — 🔑 Bearer · Rate limit: 10 req / hour

Creates a Stripe Connect Express account for the calling creator and returns a fresh onboarding link. KYC must be APPROVED before this endpoint will work — the platform requires identity verification before opening up payouts. Idempotent: if the creator already has a Stripe account, a new onboarding link is generated against it (no duplicate account creation).

KYC gate is hard. A creator with kycStatus !== 'APPROVED' can't initiate Stripe Connect — call POST /creators/kyc/start first and wait for the webhook to flip status to APPROVED. Surface "Complete identity verification first" UI when you see creator.stripe.kyc_required.

Race-safe via atomic claim. Concurrent calls don't create duplicate Stripe accounts — the first call wins via updateMany({ where: stripeAccountId: null }); subsequent racers either find the freshly-set id and return a fresh link, or fail with connect_in_progress if they slip between the claim and the persist.

Request

No body, no params. Identity comes from the bearer token.

HeaderRequiredNotes
Authorization: Bearer <accessToken>JWT from POST /auth/login

Response

201 CreatedApiResponseOf<StripeConnectInitiateResponseDto>

{
  "success": true,
  "data": {
    "accountId": "acct_...",
    "onboardingUrl": "https://connect.stripe.com/express/onboarding/..."
  }
}
FieldTypeNotes
accountIdstringStripe Express account id (saved on CreatorProfile.stripeAccountId). Treat as opaque — pass back to /refresh-link and /dashboard-link only via the platform endpoints.
onboardingUrlstringFresh hosted onboarding URL. Open in a new tab or full-page redirect. Single-use, time-limited — fetch a new one via /refresh-link if it expires.

Errors

HTTPcode / i18nKeyReason
400creator.stripe.kyc_requiredkycStatus !== 'APPROVED' — complete KYC first
400creator.stripe.connect_in_progressConcurrent call won the atomic claim before this one persisted
400creator.stripe.service_unavailableStripe SDK is not configured (server-side env / config issue) — surface "payment temporarily unavailable"
401(guard)Missing / invalid bearer token
404creator.stripe.not_creatorNo CreatorProfile for this user — call POST /creators/upgrade first
429(throttle)Rate limit exceeded (10 req/hour)

Side effects

  1. Lookup CreatorProfile (with user.email); throw not_creator if missing.
  2. KYC gate: assert kycStatus === APPROVED; otherwise kyc_required.
  3. Idempotent path — if stripeAccountId already set: skip account creation, just call createAccountLink(stripeAccountId) and return.
  4. Atomic claimupdateMany({ where: { userId, stripeAccountId: null }, data: { stripeAccountStatus: PENDING } }). Zero rows touched → another concurrent request beat us; re-read and either return its result or throw connect_in_progress.
  5. Stripe SDK creates the Express accountaccounts.create({ type: 'express', country: <stripe.connect_country admin-managed, default 'US'>, email: user.email, metadata: { creatorId, userId, platform: 'biore' }, capabilities: { transfers: { requested: true } } }).
  6. Persist stripeAccountId (update({ stripeAccountId })).
  7. Audit log: [stripe-connect] Account created: {accountId} for creator {creatorId}.
  8. Generate the onboarding URL via createAccountLink(accountId) (Stripe accountLinks.create).
  9. Return { accountId, onboardingUrl }.
  10. Webhook updates status fields — Stripe's account.updated webhook flips stripeChargesEnabled / stripePayoutsEnabled / stripeAccountStatus server-side as the creator completes onboarding (handled by an internal webhook controller, not this portal).

Code samples

curl -X POST https://api.bio.re/api/v1/creators/stripe-connect/initiate \
  -H "Authorization: Bearer $ACCESS_TOKEN"
type StripeConnectInitiate = {
  accountId: string;
  onboardingUrl: string;
};

async function initiateStripeConnect(accessToken: string): Promise<StripeConnectInitiate> {
  const res = await fetch('https://api.bio.re/api/v1/creators/stripe-connect/initiate', {
    method: 'POST',
    headers: { Authorization: `Bearer ${accessToken}` },
  });
  const json = await res.json();
  if (!res.ok || !json.success) {
    throw Object.assign(new Error(json?.error?.message ?? 'Stripe Connect initiate failed'), {
      code: json?.error?.code,
    });
  }
  return json.data;
}

// Usage: redirect to onboardingUrl in a new tab
function openOnboarding(onboardingUrl: string) {
  window.location.assign(onboardingUrl);
}
import { useMutation, useQueryClient } from '@tanstack/react-query';

export function useInitiateStripeConnect() {
  const qc = useQueryClient();
  return useMutation({
    mutationFn: async () => {
      const res = await fetch('/api/v1/creators/stripe-connect/initiate', { method: 'POST' });
      const json = await res.json();
      if (!res.ok || !json.success) {
        throw Object.assign(new Error(json?.error?.message ?? 'Stripe Connect initiate failed'), {
          code: json?.error?.code,
          i18nKey: json?.error?.i18nKey,
        });
      }
      return json.data as StripeConnectInitiate;
    },
    onSuccess: () => {
      // Status flipped to PENDING server-side
      qc.invalidateQueries({ queryKey: ['creators', 'stripe-connect', 'status'] });
      qc.invalidateQueries({ queryKey: ['creators', 'payout-settings'] });
      qc.invalidateQueries({ queryKey: ['creators', 'profile'] });
    },
  });
}

Try it

POST
/api/v1/creators/stripe-connect/initiate
AuthorizationBearer <token>

In: header

Response Body

application/json

application/json

curl -X POST "https://loading/api/v1/creators/stripe-connect/initiate"
{
  "success": true,
  "data": {
    "accountId": "acct_1234567890",
    "onboardingUrl": "string"
  }
}
{
  "success": false,
  "error": {
    "code": "AUTH_UNAUTHORIZED",
    "message": "Invalid credentials",
    "i18nKey": "auth.login.invalid_credentials",
    "i18nVars": {
      "field": "email"
    },
    "details": [
      {
        "message": "email must be an email"
      }
    ],
    "correlationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
  }
}

Source

SourcePathLines
Controllerapps/api-core/src/modules/creator/stripe-connect.controller.ts19–25 (initiate)
DTO (response)apps/api-core/src/modules/creator/dto/stripe-connect-response.dto.ts9–15 (StripeConnectInitiateResponseDto)
Serviceapps/api-core/src/modules/creator/stripe-connect.service.ts25–98 (initiateConnect)
Stripe providerapps/api-core/src/modules/payment/stripe.provider.tsgetStripe(), requireStripe()
Configapps/api-core/src/modules/config/config.service.tsstripe.connect_country (admin-managed, default 'US')
Webhook (status sync)apps/api-core/src/modules/payment/Stripe account.updated handler (internal scope)
Prisma modelpackages/prisma/prisma/schema.prismaCreatorProfile.stripeAccountId, CreatorProfile.stripeAccountStatus (enum StripeAccountStatus), CreatorProfile.kycStatus (enum KycStatus.APPROVED)

On this page