Skip to content
Tolinku
Tolinku
Sign In Start Free
Use Cases · · 5 min read

Onboarding Push Notifications: Timing and Deep Links

By Tolinku Staff
|
Tolinku fintech deep linking dashboard screenshot for use cases blog posts

Push notifications during onboarding walk a fine line. Sent at the right time with the right message, they bring users back to complete setup and reach their first value moment. Sent too early, too often, or with generic copy, they get your app muted or uninstalled. This guide covers when, what, and how to send onboarding push notifications that actually help.

For email-based onboarding, see Onboarding Email Sequences with Deep Links. For broader notification strategy, see Push Notification Strategy for Mobile Apps.

When to Send Onboarding Pushes

The Timing Rules

Scenario When to Send Deep Link Target
User left during signup 4-6 hours later /onboarding/signup
Signup complete, no first action 24 hours later /onboarding/first-action
First action done, setup incomplete 48 hours later /onboarding/resume
Haven't opened app in 3 days Day 3 /home with welcome back context
Feature not yet discovered Day 5-7 /feature/{feature_name}

What Not to Do

  • Don't send a push notification within the first hour. The user just left your app; give them space.
  • Don't send more than 1 onboarding push per day.
  • Don't send pushes after 9pm or before 8am in the user's local time.
  • Don't send a "Welcome!" push immediately after signup. The user literally just saw your app.

Permission Timing

You need push permission before you can send anything. The best time to ask:

function shouldAskPushPermission(user) {
  // Don't ask on first screen
  if (user.onboardingStep === 0) return false;

  // Ask after the user has experienced value
  if (user.hasCompletedFirstAction) return true;

  // Ask after they've spent at least 2 minutes in the app
  if (user.sessionDuration > 120000) return true;

  return false;
}

async function requestPushWithContext(reason) {
  // Show a pre-permission screen first
  const userConsent = await showPrePermission({
    title: 'Stay in the loop',
    body: reason,
    allowText: 'Enable',
    denyText: 'Not Now',
  });

  if (userConsent) {
    const granted = await requestPushPermission();
    return granted;
  }

  return false;
}

Pre-permission screens increase grant rates by 30-50% because the user understands why before the system prompt appears. For a broader look at onboarding fundamentals (including when to request permissions), see Onboarding Best Practices for Mobile Apps in 2026.

Notification Content

Copy Patterns That Work

Incomplete signup:

Title: Pick up where you left off
Body: Your account is almost ready. Just one more step.
Deep link: /onboarding/signup

No first action:

Title: Ready to create your first [item]?
Body: It takes about 30 seconds. Give it a try.
Deep link: /create

Feature discovery:

Title: Did you know you can [feature benefit]?
Body: [One sentence about the feature]. Try it now.
Deep link: /feature/sharing

Progress reminder:

Title: 2 of 3 steps done
Body: Finish your last step to unlock all features.
Deep link: /onboarding/resume

Copy Patterns That Don't Work

Bad Copy Why It Fails
"We miss you!" Needy, no value proposition
"Come back to [App]!" No reason given
"You have unread notifications" Misleading if there are none
"Don't miss out!" Vague urgency
"Hey! 👋" No substance

Every push notification should answer: "What will the user gain by tapping this?"

async function createOnboardingPushLink(userId, destination) {
  const link = await Tolinku.createLink({
    path: destination,
    params: {
      source: 'push',
      campaign: 'onboarding',
      user_id: userId,
    },
  });

  return link.url;
}
async function sendOnboardingPush(userId, notification) {
  const deepLink = await createOnboardingPushLink(userId, notification.deepLink);

  await pushService.send(userId, {
    title: notification.title,
    body: notification.body,
    data: {
      deepLink: deepLink,
      type: 'onboarding',
      campaign: notification.campaign,
    },
  });

  analytics.track('onboarding_push_sent', {
    userId,
    campaign: notification.campaign,
    deepLink: notification.deepLink,
  });
}

Handling Push Tap in the App

function handlePushNotificationTap(notification) {
  const data = notification.data;

  analytics.track('onboarding_push_tapped', {
    campaign: data.campaign,
    deepLink: data.deepLink,
  });

  if (data.deepLink) {
    // Navigate directly to the target screen
    handleDeepLink(data.deepLink);
  }
}

Scheduling Logic

The Onboarding Push Scheduler

async function scheduleOnboardingPushes(userId) {
  const user = await getUser(userId);
  const timezone = user.timezone || 'UTC';

  const pushes = [
    {
      condition: () => user.onboardingCompleted === false && user.hasAccount,
      delay: '24h',
      notification: {
        title: 'Finish setting up',
        body: 'Complete your profile to get personalized recommendations.',
        deepLink: '/onboarding/resume',
        campaign: 'onboarding_day1',
      },
    },
    {
      condition: () => user.hasCompletedFirstAction === false,
      delay: '48h',
      notification: {
        title: 'Create your first [item]',
        body: 'See why thousands of people use [App] every day.',
        deepLink: '/create',
        campaign: 'onboarding_day2',
      },
    },
    {
      condition: () => user.hasUsedFeature('sharing') === false && user.itemCount >= 1,
      delay: '5d',
      notification: {
        title: 'Share your work',
        body: 'Invite friends or colleagues to see what you have created.',
        deepLink: '/feature/sharing',
        campaign: 'onboarding_day5',
      },
    },
  ];

  for (const push of pushes) {
    schedulePush(userId, push, timezone);
  }
}

function schedulePush(userId, push, timezone) {
  const sendAt = addDuration(new Date(), push.delay);
  const localSendAt = adjustToLocalTime(sendAt, timezone, { minHour: 9, maxHour: 20 });

  pushQueue.add({
    userId,
    sendAt: localSendAt,
    notification: push.notification,
    condition: push.condition, // Re-evaluated at send time
  });
}

Re-Evaluate Conditions at Send Time

The user may have completed the action between scheduling and send time:

async function processPushQueue(job) {
  const user = await getUser(job.userId);

  // Re-check the condition
  if (job.condition(user) === false) {
    // User already completed the action; skip this push
    return;
  }

  // Check push permission is still granted
  if (user.pushEnabled === false) {
    return;
  }

  await sendOnboardingPush(job.userId, job.notification);
}

Frequency Capping

Rules

const PUSH_LIMITS = {
  maxPerDay: 1,
  maxPerWeek: 3,
  maxOnboardingTotal: 5,
  minTimeBetweenPushes: 12 * 60 * 60 * 1000, // 12 hours
};

async function canSendPush(userId) {
  const recentPushes = await getRecentPushes(userId);

  const today = recentPushes.filter(p => isToday(p.sentAt));
  if (today.length >= PUSH_LIMITS.maxPerDay) return false;

  const thisWeek = recentPushes.filter(p => isThisWeek(p.sentAt));
  if (thisWeek.length >= PUSH_LIMITS.maxPerWeek) return false;

  const onboardingPushes = recentPushes.filter(p => p.campaign.startsWith('onboarding_'));
  if (onboardingPushes.length >= PUSH_LIMITS.maxOnboardingTotal) return false;

  const lastPush = recentPushes[0];
  if (lastPush && (Date.now() - lastPush.sentAt) < PUSH_LIMITS.minTimeBetweenPushes) return false;

  return true;
}

Measuring Push Effectiveness

Key Metrics

Metric Formula Benchmark
Delivery rate Delivered / Sent > 95%
Open rate Tapped / Delivered 5-15% for onboarding
Deep link completion Reached target screen / Tapped 80-90%
Action completion Completed target action / Tapped 15-30%
Opt-out rate (per push) Disabled after push / Delivered < 0.5%

Track Push-Attributed Actions

analytics.track('onboarding_action_completed', {
  action: 'first_item_created',
  attributedTo: 'push', // or 'email', 'organic'
  pushCampaign: 'onboarding_day2',
  timeFromPush: Date.now() - lastPushTappedAt,
});

Optimization

If open rates are below 5%, the problem is timing or copy. If open rates are above 10% but action completion is below 10%, the problem is the deep link target or the in-app experience after the tap. For users who've stopped responding to onboarding pushes entirely, transition them to a dedicated re-engagement campaign.

For deep linking features, see Tolinku deep linking. For onboarding use cases, see the onboarding documentation.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.