Post-Install Actions & Onboarding

Post-install actions determine what happens immediately after a user installs your Stripe App. A well-designed post-install experience guides users through setup and increases activation rates.

Post-Install Action Types

Stripe supports four post-install action types, each configured in your app manifest:

Opens the app in the default drawer viewport. This is the default behavior if no post_install_action is specified:

{
"post_install_action": {
"type": "default"
}
}

The user sees the app’s drawer.default viewport in the Stripe Dashboard sidebar.

Opens the app’s dedicated onboarding view, providing a focused setup experience:

{
"post_install_action": {
"type": "onboarding"
}
}

This requires an onboarding viewport declared in your manifest:

{
"ui_extension": {
"views": [
{
"viewport": "stripe.dashboard.onboarding",
"component": "OnboardingView"
}
]
},
"post_install_action": {
"type": "onboarding"
}
}

Opens the app’s settings view, useful when the app requires API keys or configuration before use:

{
"post_install_action": {
"type": "settings"
}
}

This requires a settings viewport:

{
"ui_extension": {
"views": [
{
"viewport": "stripe.dashboard.settings",
"component": "SettingsView"
}
]
},
"post_install_action": {
"type": "settings"
}
}

Redirects the user to an external URL for setup. Use this when your onboarding flow lives outside the Stripe Dashboard:

{
"post_install_action": {
"type": "external",
"url": "https://app.tajo.io/stripe/setup"
}
}

Caution

External URLs must use HTTPS and should be listed in your allowed_redirect_uris. The Stripe review team will verify that the external URL provides a functional setup experience.

Onboarding Best Practices

Make It Effortless

Minimize the number of steps required to get started:

  • Pre-fill information available from the Stripe account context
  • Use sensible defaults for configuration options
  • Allow skipping optional steps with a clear path to complete them later
  • Show progress with step indicators for multi-step flows

Make It Customizable

Let users configure the integration to their needs:

  • Data mapping options — let users choose which Stripe fields sync to Brevo
  • Sync frequency — offer real-time, hourly, or daily sync options
  • Selective sync — let users choose which customers or products to sync
  • Notification preferences — configure alerts for sync errors or important events

Make It Relevant

Show value immediately:

  • Preview synced data before enabling the integration
  • Show what will happen when the user completes setup
  • Provide a test sync option to verify the connection works
  • Display success metrics after the initial sync completes

OnboardingView Component

The OnboardingView component renders in a focused modal when the user installs the app:

import {
Box,
Button,
Inline,
Icon,
Banner,
TextField,
Select,
Divider,
} from '@stripe/ui-extension-sdk/ui';
import type { ExtensionContextValue } from '@stripe/ui-extension-sdk/context';
import { useState } from 'react';
const OnboardingView = ({ environment, userContext }: ExtensionContextValue) => {
const [step, setStep] = useState(1);
const [brevoApiKey, setBrevoApiKey] = useState('');
const [syncMode, setSyncMode] = useState('realtime');
const [isConnecting, setIsConnecting] = useState(false);
const [error, setError] = useState<string | null>(null);
const totalSteps = 3;
const handleConnect = async () => {
setIsConnecting(true);
setError(null);
try {
// Store the API key securely
await storeBrevoApiKey(brevoApiKey);
// Verify the connection
const result = await verifyBrevoConnection(brevoApiKey);
if (result.success) {
setStep(2);
} else {
setError('Unable to connect to Brevo. Please check your API key.');
}
} catch (err) {
setError('Connection failed. Please try again.');
} finally {
setIsConnecting(false);
}
};
return (
<Box css={{ padding: 'large' }}>
{/* Progress indicator */}
<Inline css={{ marginBottom: 'large' }}>
Step {step} of {totalSteps}
</Inline>
{error && (
<Banner type="critical" title="Connection Error">
{error}
</Banner>
)}
{step === 1 && (
<Box>
<Inline css={{ fontWeight: 'bold', fontSize: 'large' }}>
Connect Your Brevo Account
</Inline>
<Inline css={{ marginTop: 'small', color: 'secondary' }}>
Enter your Brevo API key to start syncing customer data.
</Inline>
<TextField
label="Brevo API Key"
placeholder="xkeysib-..."
value={brevoApiKey}
onChange={(e) => setBrevoApiKey(e.target.value)}
css={{ marginTop: 'medium' }}
/>
<Inline css={{ marginTop: 'xsmall', color: 'secondary', fontSize: 'small' }}>
Find your API key in Brevo under Settings &gt; SMTP &amp; API &gt; API Keys
</Inline>
<Button
type="primary"
onPress={handleConnect}
disabled={!brevoApiKey || isConnecting}
css={{ marginTop: 'medium' }}
>
{isConnecting ? 'Connecting...' : 'Connect Brevo'}
</Button>
</Box>
)}
{step === 2 && (
<Box>
<Inline css={{ fontWeight: 'bold', fontSize: 'large' }}>
Configure Sync Settings
</Inline>
<Select
label="Sync Mode"
value={syncMode}
onChange={(value) => setSyncMode(value)}
css={{ marginTop: 'medium' }}
>
<option value="realtime">Real-time (recommended)</option>
<option value="hourly">Every hour</option>
<option value="daily">Once per day</option>
</Select>
<Divider css={{ marginY: 'medium' }} />
<Button type="primary" onPress={() => setStep(3)}>
Continue
</Button>
<Button type="secondary" onPress={() => setStep(1)}>
Back
</Button>
</Box>
)}
{step === 3 && (
<Box>
<Banner type="default" title="Ready to Sync">
Your Brevo account is connected. Tajo will begin syncing
customer data automatically.
</Banner>
<Box css={{ marginTop: 'medium' }}>
<Inline css={{ fontWeight: 'bold' }}>What happens next:</Inline>
<ul>
<li>Existing Stripe customers will sync to Brevo contacts</li>
<li>New customers and events will sync in real-time</li>
<li>View sync status on any customer's detail page</li>
</ul>
</Box>
<Button type="primary" onPress={() => {/* Navigate to dashboard */}}>
Go to Dashboard
</Button>
</Box>
)}
</Box>
);
};
export default OnboardingView;

Sign-In Flow with SignInView

If your app requires users to sign in to an external account (like Tajo), use a dedicated sign-in view:

import {
Box,
Button,
Inline,
TextField,
Banner,
Link,
} from '@stripe/ui-extension-sdk/ui';
import { useState } from 'react';
const SignInView = ({ onSignInComplete }) => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSignIn = async () => {
setIsLoading(true);
setError(null);
try {
const response = await fetch('https://api.tajo.io/v1/auth/stripe-app', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
throw new Error('Invalid credentials');
}
const { token } = await response.json();
// Store the auth token securely in Stripe's Secret Store
await storeAuthToken(token);
onSignInComplete();
} catch (err) {
setError('Sign-in failed. Please check your credentials and try again.');
} finally {
setIsLoading(false);
}
};
return (
<Box css={{ padding: 'large' }}>
<Inline css={{ fontWeight: 'bold', fontSize: 'large' }}>
Sign in to Tajo
</Inline>
<Inline css={{ marginTop: 'small', color: 'secondary' }}>
Connect your Tajo account to enable Brevo sync.
</Inline>
{error && (
<Banner type="critical" title="Sign-in Failed">
{error}
</Banner>
)}
<TextField
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
css={{ marginTop: 'medium' }}
/>
<TextField
label="Password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
css={{ marginTop: 'small' }}
/>
<Button
type="primary"
onPress={handleSignIn}
disabled={!email || !password || isLoading}
css={{ marginTop: 'medium' }}
>
{isLoading ? 'Signing in...' : 'Sign In'}
</Button>
<Link href="https://app.tajo.io/signup" external css={{ marginTop: 'small' }}>
Don't have a Tajo account? Sign up
</Link>
</Box>
);
};

You can launch specific onboarding steps or pre-fill data using query parameters in deep links:

import type { ExtensionContextValue } from '@stripe/ui-extension-sdk/context';
const OnboardingView = ({ environment }: ExtensionContextValue) => {
// Access query parameters from the deep link
const { queryParams } = environment;
// Pre-fill step from query parameter
const initialStep = queryParams?.step ? parseInt(queryParams.step) : 1;
// Pre-fill API key from query parameter (e.g., from Tajo dashboard)
const prefilledApiKey = queryParams?.brevo_key || '';
// Source tracking for analytics
const installSource = queryParams?.source || 'marketplace';
const [step, setStep] = useState(initialStep);
const [brevoApiKey, setBrevoApiKey] = useState(prefilledApiKey);
// ... rest of onboarding logic
};

Generate deep links that pre-fill onboarding data:

// From your Tajo dashboard, generate a link that pre-fills the Brevo API key
const onboardingLink = [
'https://dashboard.stripe.com/live/acct_xxxxx/dashboard',
'?apps[com.tajo.brevo-integration][modal]=stripe.dashboard.onboarding',
'&apps[com.tajo.brevo-integration][queryParams][step]=1',
'&apps[com.tajo.brevo-integration][queryParams][source]=tajo_dashboard',
].join('');

Handling Returning Users

When a user opens your app after completing onboarding, detect their state and show the appropriate view:

const MainView = ({ environment, userContext }: ExtensionContextValue) => {
const [authState, setAuthState] = useState<'loading' | 'signed-out' | 'onboarding' | 'ready'>('loading');
useEffect(() => {
checkUserState().then((state) => {
setAuthState(state);
});
}, []);
switch (authState) {
case 'loading':
return <Spinner label="Loading..." />;
case 'signed-out':
return <SignInView onSignInComplete={() => setAuthState('onboarding')} />;
case 'onboarding':
return <OnboardingView onComplete={() => setAuthState('ready')} />;
case 'ready':
return <DashboardView />;
}
};

Tip

Store onboarding completion status in the Stripe Secret Store so you can detect returning users without an external API call.

AI Assistant

Hi! Ask me anything about the docs.

Start Free with Brevo