Advanced

Deep Linking into the Paddle Customer Portal

The customer portal now supports deep linking functionality that allows you to create direct links to specific actions and pages within the portal. This feature is particularly useful for:

  • Marketing campaigns directing customers to upgrade/downgrade plans
  • Support teams helping customers navigate directly to specific features
  • Email campaigns with targeted calls-to-action
  • Reducing friction in the customer journey by minimizing clicks

We recommend using the Boathouse API to fetch a billing portal link for your logged-in user where you can then append the deep-linking parameters below (they do not affect the signature). But here we will show you how to manually create that link for a user.

Prerequisites

Before using deep linking features, make sure you understand how to create signed billing portal links. Deep linking parameters are added to the basic signed portal URL.

Base Portal URL Structure

The basic customer portal URL with deep linking parameters follows this structure:

https://my.boathouse.co/portal?p={portalId}&c={customerId}&s={signature}&e={expiresTimestamp}&r={returnUrl}&{deepLinkParameters}

Deep Linking Parameters

Subscription Selection (subscription_id)

Directly select a specific subscription for actions:

https://my.boathouse.co/portal?c={customerID}&e={expiry}&p={portalID}&r={returnUrl}&s={signature}&subscription_id={subscriptionId}

Use Cases:

  • Direct customer to manage a specific subscription
  • Open change plan UI for a particular subscription
  • Useful when customer has multiple subscriptions

Behavior:

  • Opens the portal with the specified subscription selected
  • If used alone, opens the change plan interface for that subscription
  • If subscription ID doesn't exist or doesn't belong to the customer, it's ignored

Target Price Selection (target_price_id)

Automatically navigate to plan change with a specific price pre-selected:

https://my.boathouse.co/portal?c={customerID}&e={expiry}&p={portalID}&r={returnUrl}&s={signature}&target_price_id={targetPriceId}

Use Cases:

  • Marketing campaigns for specific plan upgrades
  • Promotional emails for plan changes
  • Upsell/downsell workflows
  • Reducing friction for planned migrations

Behavior:

  • Automatically opens the change plan interface
  • Pre-selects the specified price/plan
  • Navigates directly to the confirmation stage
  • Shows preview of pricing changes immediately
  • If no subscription_id is provided, defaults to the first available subscription

Action Parameter (a)

Control which panel opens in the portal interface:

https://my.boathouse.co/portal?c={customerID}&e={expiry}&p={portalID}&r={returnUrl}&s={signature}&a={action}

Available Actions:

  • change_plan - Opens the plan change interface
  • confirm_change_plan - Opens plan change with confirmation (use with target_price_id)
  • pause_subscription - Opens the pause subscription interface (or resume if already paused)
  • cancel_subscription - Opens the cancel subscription interface
  • change_quantity - Opens the change quantity/licenses interface
  • change_billing_information - Opens the billing information update interface
  • list_invoices - Shows only the invoices panel (can be filtered by subscription_id)
  • cancel_downgrade - Opens confirmation to cancel a scheduled downgrade

Case Sensitivity

The action parameter uses snake_case enum values. ASP.NET Core's model binding is case-insensitive, so change_plan, Change_Plan, or CHANGE_PLAN will all work.

Combined Deep Linking

You can combine parameters for precise control:

https://my.boathouse.co/portal?c={customerID}&e={expiry}&p={portalID}&r={returnUrl}&s={signature}&subscription_id={subscriptionId}&target_price_id={targetPriceId}&a=confirm_change_plan

This will:

  1. Select the specific subscription
  2. Open the change plan interface
  3. Pre-select the target price
  4. Show the confirmation screen with pricing preview

Implementation Examples

Upgrade Campaign

Create a link for customers to upgrade to your "Pro Annual" plan:

https://my.boathouse.co/portal?p=...&target_price_id=pri_professional_annual_2024&a=confirm_change_plan

When clicked, this will:

  • Open the customer portal
  • Automatically select their subscription (or first one if multiple)
  • Pre-select the Professional Annual plan
  • Show the pricing change confirmation immediately

Support Team Assistance

Help a customer with a specific subscription change:

https://my.boathouse.co/portal?p=...&subscription_id=sub_XYZ789&target_price_id=pri_basic_monthly_2024&a=confirm_change_plan

This provides precise control to:

  • Target the exact subscription (useful for customers with multiple)
  • Pre-select the plan they discussed with support
  • Minimize confusion and steps needed

Simple Plan Change Navigation

Just open the change plan interface for easy browsing:

https://my.boathouse.co/portal?p=...&subscription_id=sub_XYZ789&a=change_plan

This will:

  • Open the change plan interface for the specified subscription
  • Allow the customer to browse available plans
  • No pre-selection, giving full control to the customer

Pause or Cancel Subscription

Direct link to pause a subscription:

https://my.boathouse.co/portal?p=...&subscription_id=sub_XYZ789&a=pause_subscription

If the subscription is already paused, this will show a resume screen instead.

Direct link to cancel a subscription:

https://my.boathouse.co/portal?p=...&subscription_id=sub_XYZ789&a=cancel_subscription

Update Billing Information

Direct link to update payment method:

https://my.boathouse.co/portal?p=...&subscription_id=sub_XYZ789&a=change_billing_information

Cancel Scheduled Downgrade

Direct link to cancel a scheduled downgrade:

https://my.boathouse.co/portal?p=...&subscription_id=sub_XYZ789&a=cancel_downgrade

Use Case: When a customer has scheduled a downgrade but you want to offer them an easy way to keep their current plan (e.g., in a retention email campaign).

View Invoices Only

Direct link to view all invoices:

https://my.boathouse.co/portal?p=...&a=list_invoices

Filter invoices for a specific subscription:

https://my.boathouse.co/portal?p=...&subscription_id=sub_XYZ789&a=list_invoices

The list_invoices action displays only the invoices panel without any subscription information. When subscription_id is provided, it filters the invoices to show only those related to the specified subscription.

Code Examples

C# / .NET with Deep Linking

using System.Security.Cryptography;
using System.Text;
using System.Web;

public class DeepLinkedPortal : PortalHmac
{
    public static string CreateUpgradeLink(
        string portalSecret,
        Guid portalID,
        string customerID,
        string returnUrl,
        string targetPriceId,
        long expiresAfterUnixTimestamp)
    {
        var baseQuery = GetQueryString(portalID, customerID, returnUrl, expiresAfterUnixTimestamp);
        var signature = HttpUtility.UrlEncode(
            GetSignature(portalSecret, portalID, customerID, returnUrl, expiresAfterUnixTimestamp)
        );

        return $"https://my.boathouse.co/portal?{baseQuery}&s={signature}&target_price_id={targetPriceId}&a=confirm_change_plan";
    }

    public static string CreateSubscriptionManagementLink(
        string portalSecret,
        Guid portalID,
        string customerID,
        string returnUrl,
        string subscriptionId,
        string action,
        long expiresAfterUnixTimestamp)
    {
        var baseQuery = GetQueryString(portalID, customerID, returnUrl, expiresAfterUnixTimestamp);
        var signature = HttpUtility.UrlEncode(
            GetSignature(portalSecret, portalID, customerID, returnUrl, expiresAfterUnixTimestamp)
        );

        return $"https://my.boathouse.co/portal?{baseQuery}&s={signature}&subscription_id={subscriptionId}&a={action}";
    }
}

Node.js with Deep Linking

const crypto = require('crypto')

const createDeepLinkedPortalUrl = (customerId, deepLinkParams = {}) => {
  const expiration = Math.floor(Date.now() / 1000) + 60 * 60 // 1 hour
  const returnUrl = encodeURIComponent('https://example.com').toLowerCase()
  const query = `c=${customerId}&e=${expiration}&p=${boathousePortalId}&r=${returnUrl}`

  const signature = crypto
    .createHmac('sha256', boathouseSecret)
    .update(query)
    .digest('base64')

  let url = `https://my.boathouse.co/portal?${query}&s=${encodeURIComponent(
    signature,
  )}`

  // Add deep linking parameters
  if (deepLinkParams.subscription_id) {
    url += `&subscription_id=${deepLinkParams.subscription_id}`
  }
  if (deepLinkParams.target_price_id) {
    url += `&target_price_id=${deepLinkParams.target_price_id}`
  }
  if (deepLinkParams.action) {
    url += `&a=${deepLinkParams.action}`
  }

  return url
}

// Usage examples:
const upgradeLink = createDeepLinkedPortalUrl('c123', {
  target_price_id: 'pri_professional_annual_2024',
  action: 'confirm_change_plan',
})

const pauseLink = createDeepLinkedPortalUrl('c123', {
  subscription_id: 'sub_XYZ789',
  action: 'pause_subscription',
})

PHP with Deep Linking

function createDeepLinkedPortalUrl($customerId, $deepLinkParams = []) {
    global $boathousePortalId, $boathouseSecret;

    $expiration = time() + 60 * 60; // 1 hour
    $returnUrl = strtolower(urlencode('https://example.com'));
    $query = "c={$customerId}&e={$expiration}&p={$boathousePortalId}&r={$returnUrl}";

    $signature = base64_encode(hash_hmac('sha256', $query, $boathouseSecret, true));
    $url = "https://my.boathouse.co/portal?{$query}&s=" . urlencode($signature);

    // Add deep linking parameters
    if (isset($deepLinkParams['subscription_id'])) {
        $url .= "&subscription_id=" . $deepLinkParams['subscription_id'];
    }
    if (isset($deepLinkParams['target_price_id'])) {
        $url .= "&target_price_id=" . $deepLinkParams['target_price_id'];
    }
    if (isset($deepLinkParams['action'])) {
        $url .= "&a=" . $deepLinkParams['action'];
    }

    return $url;
}

// Usage examples:
$upgradeLink = createDeepLinkedPortalUrl('c123', [
    'target_price_id' => 'pri_professional_annual_2024',
    'action' => 'confirm_change_plan'
]);

$cancelLink = createDeepLinkedPortalUrl('c123', [
    'subscription_id' => 'sub_XYZ789',
    'action' => 'cancel_subscription'
]);

Security Considerations

Important Security Note

The deep linking parameters (subscription_id, target_price_id, and a) are NOT included in the HMAC signature calculation. This is intentional and secure because:

  • Authentication remains intact: The core authentication parameters (portal ID, customer ID, expiration) are still validated
  • Flexibility: The same signed URL can be reused with different navigation parameters
  • No security risk: These parameters only control navigation within the already-authenticated session
  • Customer isolation: Customers can only see and modify their own subscriptions

Parameter Validation

The system validates:

  • Subscription ID must belong to the authenticated customer
  • Target price ID must be a valid, active price in the portal
  • If invalid parameters are provided, they are silently ignored
  • The portal falls back to default behavior for invalid parameters

This ensures that even if someone manipulates the deep linking parameters, they cannot access or modify data that doesn't belong to the authenticated customer.

Previous
Signing Billing Portal Links