{"id":993,"date":"2026-05-05T09:00:00","date_gmt":"2026-05-05T14:00:00","guid":{"rendered":"https:\/\/tolinku.com\/blog\/?p=993"},"modified":"2026-03-07T04:46:26","modified_gmt":"2026-03-07T09:46:26","slug":"onboarding-push-notifications","status":"publish","type":"post","link":"https:\/\/tolinku.com\/blog\/onboarding-push-notifications\/","title":{"rendered":"Onboarding Push Notifications: Timing and Deep Links"},"content":{"rendered":"\n<p>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.<\/p>\n\n\n\n<p>For email-based onboarding, see <a href=\"https:\/\/tolinku.com\/blog\/onboarding-email-sequences\/\">Onboarding Email Sequences with Deep Links<\/a>. For broader notification strategy, see <a href=\"https:\/\/tolinku.com\/blog\/push-notification-strategy\/\">Push Notification Strategy for Mobile Apps<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">When to Send Onboarding Pushes<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">The Timing Rules<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Scenario<\/th>\n<th>When to Send<\/th>\n<th>Deep Link Target<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>User left during signup<\/td>\n<td>4-6 hours later<\/td>\n<td><code>\/onboarding\/signup<\/code><\/td>\n<\/tr>\n<tr>\n<td>Signup complete, no first action<\/td>\n<td>24 hours later<\/td>\n<td><code>\/onboarding\/first-action<\/code><\/td>\n<\/tr>\n<tr>\n<td>First action done, setup incomplete<\/td>\n<td>48 hours later<\/td>\n<td><code>\/onboarding\/resume<\/code><\/td>\n<\/tr>\n<tr>\n<td>Haven&#39;t opened app in 3 days<\/td>\n<td>Day 3<\/td>\n<td><code>\/home<\/code> with welcome back context<\/td>\n<\/tr>\n<tr>\n<td>Feature not yet discovered<\/td>\n<td>Day 5-7<\/td>\n<td><code>\/feature\/{feature_name}<\/code><\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">What Not to Do<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Don&#39;t send a push notification within the first hour. The user just left your app; give them space.<\/li>\n<li>Don&#39;t send more than 1 onboarding push per day.<\/li>\n<li>Don&#39;t send pushes after 9pm or before 8am in the user&#39;s local time.<\/li>\n<li>Don&#39;t send a &quot;Welcome!&quot; push immediately after signup. The user literally just saw your app.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Permission Timing<\/h3>\n\n\n\n<p>You need push permission before you can send anything. The best time to ask:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">function shouldAskPushPermission(user) {\n  \/\/ Don&#39;t ask on first screen\n  if (user.onboardingStep === 0) return false;\n\n  \/\/ Ask after the user has experienced value\n  if (user.hasCompletedFirstAction) return true;\n\n  \/\/ Ask after they&#39;ve spent at least 2 minutes in the app\n  if (user.sessionDuration &gt; 120000) return true;\n\n  return false;\n}\n\nasync function requestPushWithContext(reason) {\n  \/\/ Show a pre-permission screen first\n  const userConsent = await showPrePermission({\n    title: &#39;Stay in the loop&#39;,\n    body: reason,\n    allowText: &#39;Enable&#39;,\n    denyText: &#39;Not Now&#39;,\n  });\n\n  if (userConsent) {\n    const granted = await requestPushPermission();\n    return granted;\n  }\n\n  return false;\n}\n<\/code><\/pre>\n\n\n\n<p>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 <a href=\"https:\/\/tolinku.com\/blog\/onboarding-best-practices-2026\/\">Onboarding Best Practices for Mobile Apps in 2026<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Notification Content<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Copy Patterns That Work<\/h3>\n\n\n\n<p><strong>Incomplete signup:<\/strong><\/p>\n\n\n\n<pre><code>Title: Pick up where you left off\nBody: Your account is almost ready. Just one more step.\nDeep link: \/onboarding\/signup\n<\/code><\/pre>\n\n\n\n<p><strong>No first action:<\/strong><\/p>\n\n\n\n<pre><code>Title: Ready to create your first [item]?\nBody: It takes about 30 seconds. Give it a try.\nDeep link: \/create\n<\/code><\/pre>\n\n\n\n<p><strong>Feature discovery:<\/strong><\/p>\n\n\n\n<pre><code>Title: Did you know you can [feature benefit]?\nBody: [One sentence about the feature]. Try it now.\nDeep link: \/feature\/sharing\n<\/code><\/pre>\n\n\n\n<p><strong>Progress reminder:<\/strong><\/p>\n\n\n\n<pre><code>Title: 2 of 3 steps done\nBody: Finish your last step to unlock all features.\nDeep link: \/onboarding\/resume\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Copy Patterns That Don&#39;t Work<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Bad Copy<\/th>\n<th>Why It Fails<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>&quot;We miss you!&quot;<\/td>\n<td>Needy, no value proposition<\/td>\n<\/tr>\n<tr>\n<td>&quot;Come back to [App]!&quot;<\/td>\n<td>No reason given<\/td>\n<\/tr>\n<tr>\n<td>&quot;You have unread notifications&quot;<\/td>\n<td>Misleading if there are none<\/td>\n<\/tr>\n<tr>\n<td>&quot;Don&#39;t miss out!&quot;<\/td>\n<td>Vague urgency<\/td>\n<\/tr>\n<tr>\n<td>&quot;Hey! \ud83d\udc4b&quot;<\/td>\n<td>No substance<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<p>Every push notification should answer: &quot;What will the user gain by tapping this?&quot;<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Deep Link Implementation<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Generating Deep Links for Push<\/h3>\n\n\n\n<pre><code class=\"language-javascript\">async function createOnboardingPushLink(userId, destination) {\n  const link = await Tolinku.createLink({\n    path: destination,\n    params: {\n      source: &#39;push&#39;,\n      campaign: &#39;onboarding&#39;,\n      user_id: userId,\n    },\n  });\n\n  return link.url;\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Sending the Push with Deep Link<\/h3>\n\n\n\n<pre><code class=\"language-javascript\">async function sendOnboardingPush(userId, notification) {\n  const deepLink = await createOnboardingPushLink(userId, notification.deepLink);\n\n  await pushService.send(userId, {\n    title: notification.title,\n    body: notification.body,\n    data: {\n      deepLink: deepLink,\n      type: &#39;onboarding&#39;,\n      campaign: notification.campaign,\n    },\n  });\n\n  analytics.track(&#39;onboarding_push_sent&#39;, {\n    userId,\n    campaign: notification.campaign,\n    deepLink: notification.deepLink,\n  });\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Handling Push Tap in the App<\/h3>\n\n\n\n<pre><code class=\"language-javascript\">function handlePushNotificationTap(notification) {\n  const data = notification.data;\n\n  analytics.track(&#39;onboarding_push_tapped&#39;, {\n    campaign: data.campaign,\n    deepLink: data.deepLink,\n  });\n\n  if (data.deepLink) {\n    \/\/ Navigate directly to the target screen\n    handleDeepLink(data.deepLink);\n  }\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Scheduling Logic<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">The Onboarding Push Scheduler<\/h3>\n\n\n\n<pre><code class=\"language-javascript\">async function scheduleOnboardingPushes(userId) {\n  const user = await getUser(userId);\n  const timezone = user.timezone || &#39;UTC&#39;;\n\n  const pushes = [\n    {\n      condition: () =&gt; user.onboardingCompleted === false &amp;&amp; user.hasAccount,\n      delay: &#39;24h&#39;,\n      notification: {\n        title: &#39;Finish setting up&#39;,\n        body: &#39;Complete your profile to get personalized recommendations.&#39;,\n        deepLink: &#39;\/onboarding\/resume&#39;,\n        campaign: &#39;onboarding_day1&#39;,\n      },\n    },\n    {\n      condition: () =&gt; user.hasCompletedFirstAction === false,\n      delay: &#39;48h&#39;,\n      notification: {\n        title: &#39;Create your first [item]&#39;,\n        body: &#39;See why thousands of people use [App] every day.&#39;,\n        deepLink: &#39;\/create&#39;,\n        campaign: &#39;onboarding_day2&#39;,\n      },\n    },\n    {\n      condition: () =&gt; user.hasUsedFeature(&#39;sharing&#39;) === false &amp;&amp; user.itemCount &gt;= 1,\n      delay: &#39;5d&#39;,\n      notification: {\n        title: &#39;Share your work&#39;,\n        body: &#39;Invite friends or colleagues to see what you have created.&#39;,\n        deepLink: &#39;\/feature\/sharing&#39;,\n        campaign: &#39;onboarding_day5&#39;,\n      },\n    },\n  ];\n\n  for (const push of pushes) {\n    schedulePush(userId, push, timezone);\n  }\n}\n\nfunction schedulePush(userId, push, timezone) {\n  const sendAt = addDuration(new Date(), push.delay);\n  const localSendAt = adjustToLocalTime(sendAt, timezone, { minHour: 9, maxHour: 20 });\n\n  pushQueue.add({\n    userId,\n    sendAt: localSendAt,\n    notification: push.notification,\n    condition: push.condition, \/\/ Re-evaluated at send time\n  });\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Re-Evaluate Conditions at Send Time<\/h3>\n\n\n\n<p>The user may have completed the action between scheduling and send time:<\/p>\n\n\n\n<pre><code class=\"language-javascript\">async function processPushQueue(job) {\n  const user = await getUser(job.userId);\n\n  \/\/ Re-check the condition\n  if (job.condition(user) === false) {\n    \/\/ User already completed the action; skip this push\n    return;\n  }\n\n  \/\/ Check push permission is still granted\n  if (user.pushEnabled === false) {\n    return;\n  }\n\n  await sendOnboardingPush(job.userId, job.notification);\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Frequency Capping<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Rules<\/h3>\n\n\n\n<pre><code class=\"language-javascript\">const PUSH_LIMITS = {\n  maxPerDay: 1,\n  maxPerWeek: 3,\n  maxOnboardingTotal: 5,\n  minTimeBetweenPushes: 12 * 60 * 60 * 1000, \/\/ 12 hours\n};\n\nasync function canSendPush(userId) {\n  const recentPushes = await getRecentPushes(userId);\n\n  const today = recentPushes.filter(p =&gt; isToday(p.sentAt));\n  if (today.length &gt;= PUSH_LIMITS.maxPerDay) return false;\n\n  const thisWeek = recentPushes.filter(p =&gt; isThisWeek(p.sentAt));\n  if (thisWeek.length &gt;= PUSH_LIMITS.maxPerWeek) return false;\n\n  const onboardingPushes = recentPushes.filter(p =&gt; p.campaign.startsWith(&#39;onboarding_&#39;));\n  if (onboardingPushes.length &gt;= PUSH_LIMITS.maxOnboardingTotal) return false;\n\n  const lastPush = recentPushes[0];\n  if (lastPush &amp;&amp; (Date.now() - lastPush.sentAt) &lt; PUSH_LIMITS.minTimeBetweenPushes) return false;\n\n  return true;\n}\n<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Measuring Push Effectiveness<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Key Metrics<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table>\n<thead>\n<tr>\n<th>Metric<\/th>\n<th>Formula<\/th>\n<th>Benchmark<\/th>\n<\/tr>\n<\/thead>\n<tbody><tr>\n<td>Delivery rate<\/td>\n<td>Delivered \/ Sent<\/td>\n<td>&gt; 95%<\/td>\n<\/tr>\n<tr>\n<td>Open rate<\/td>\n<td>Tapped \/ Delivered<\/td>\n<td>5-15% for onboarding<\/td>\n<\/tr>\n<tr>\n<td>Deep link completion<\/td>\n<td>Reached target screen \/ Tapped<\/td>\n<td>80-90%<\/td>\n<\/tr>\n<tr>\n<td>Action completion<\/td>\n<td>Completed target action \/ Tapped<\/td>\n<td>15-30%<\/td>\n<\/tr>\n<tr>\n<td>Opt-out rate (per push)<\/td>\n<td>Disabled after push \/ Delivered<\/td>\n<td>&lt; 0.5%<\/td>\n<\/tr>\n<\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Track Push-Attributed Actions<\/h3>\n\n\n\n<pre><code class=\"language-javascript\">analytics.track(&#39;onboarding_action_completed&#39;, {\n  action: &#39;first_item_created&#39;,\n  attributedTo: &#39;push&#39;, \/\/ or &#39;email&#39;, &#39;organic&#39;\n  pushCampaign: &#39;onboarding_day2&#39;,\n  timeFromPush: Date.now() - lastPushTappedAt,\n});\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Optimization<\/h3>\n\n\n\n<p>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&#39;ve stopped responding to onboarding pushes entirely, transition them to a dedicated <a href=\"https:\/\/tolinku.com\/blog\/re-engagement-campaigns\/\">re-engagement campaign<\/a>.<\/p>\n\n\n\n<p>For deep linking features, see <a href=\"https:\/\/tolinku.com\/features\/deep-linking\">Tolinku deep linking<\/a>. For onboarding use cases, see the <a href=\"https:\/\/tolinku.com\/docs\/use-cases\/onboarding\/\">onboarding documentation<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Send push notifications that guide new users through onboarding. Get the timing, copy, and deep link targeting right to drive activation without annoying users.<\/p>\n","protected":false},"author":2,"featured_media":992,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"Onboarding Push Notifications: Timing and Deep Links","rank_math_description":"Send push notifications that guide new users through onboarding. Get the timing, copy, and deep link targeting right to drive activation without annoying users.","rank_math_focus_keyword":"onboarding push notifications","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_facebook_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-onboarding-push-notifications.png","rank_math_facebook_image_id":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_twitter_image":"https:\/\/tolinku.com\/blog\/wp-content\/uploads\/2026\/03\/og-onboarding-push-notifications.png","footnotes":""},"categories":[18],"tags":[20,223,69,27,84,120,47,33],"class_list":["post-993","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-use-cases","tag-deep-linking","tag-engagement","tag-mobile-development","tag-onboarding","tag-push-notifications","tag-re-engagement","tag-retention","tag-user-experience"],"_links":{"self":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/993","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/comments?post=993"}],"version-history":[{"count":4,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/993\/revisions"}],"predecessor-version":[{"id":2831,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/posts\/993\/revisions\/2831"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media\/992"}],"wp:attachment":[{"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/media?parent=993"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/categories?post=993"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/tolinku.com\/blog\/wp-json\/wp\/v2\/tags?post=993"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}