Skip to main content
POST
/
api
/
billing
/
create-portal-session
curl -X POST https://www.wiseyield.co/api/billing/create-portal-session \
  -H "Authorization: Bearer wy_YOUR_API_KEY"
{
  "url": "https://portal.dodopayments.com/session/ps_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}

Documentation Index

Fetch the complete documentation index at: https://docs.wiseyield.co/llms.txt

Use this file to discover all available pages before exploring further.

Endpoint

POST https://www.wiseyield.co/api/billing/create-portal-session

Authentication

Requires a valid API key with billing:write scope.

Request Body

No request body required.

Response

Returns a Dodo Payments billing portal URL.
url
string
Dodo Payments billing portal URL to redirect user to
curl -X POST https://www.wiseyield.co/api/billing/create-portal-session \
  -H "Authorization: Bearer wy_YOUR_API_KEY"
{
  "url": "https://portal.dodopayments.com/session/ps_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}

Errors

401
error
Unauthorized - Missing or invalid API key
404
error
Not Found - User has no billing customer ID (never subscribed)
500
error
Internal Server Error - Failed to create Dodo Payments portal session

How It Works

Portal Flow:
  1. API Call: Client requests portal session
  2. Customer Lookup: System finds user’s Dodo Payments customer ID
  3. Session Creation: Dodo Payments portal session created with return URL
  4. Redirect: User redirected to Dodo Payments portal
  5. Self-Service: User manages subscription on Dodo Payments
  6. Return: User clicks “Return to…” and comes back to app
Return URL: https://www.wiseyield.co/dashboard/billing Customer Requirement: User must have subscribed at least once. If no subscription history exists, returns 404 error.

Customer Portal Features

Dodo Payments portal allows users to:

Subscription Management

  • Change subscription plan (upgrade/downgrade)
  • Cancel subscription
  • Resume canceled subscription
  • View upcoming invoice

Payment Methods

  • Update credit card
  • Add new payment method
  • Set default payment method
  • Remove old payment methods

Billing History

  • View all past invoices
  • Download invoice PDFs
  • See payment history

Contact Information

  • Update billing email
  • Update billing address
  • Update tax ID

Use Cases

Open Billing Portal

async function manageBilling() {
  const response = await fetch('/api/billing/create-portal-session', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${apiKey}` }
  });

  if (!response.ok) {
    if (response.status === 404) {
      toast.error('No subscription found. Subscribe first!');
      router.push('/pricing');
      return;
    }

    toast.error('Failed to open billing portal');
    return;
  }

  const { url } = await response.json();
  window.location.href = url;
}

Manage Subscription Button

function ManageSubscriptionButton() {
  const [loading, setLoading] = useState(false);

  async function handleClick() {
    setLoading(true);
    try {
      const response = await fetch('/api/billing/create-portal-session', {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${apiKey}` }
      });

      if (!response.ok) {
        toast.error('Unable to open billing portal');
        return;
      }

      const { url } = await response.json();
      window.location.href = url;
    } finally {
      setLoading(false);
    }
  }

  return (
    <button onClick={handleClick} disabled={loading}>
      {loading ? 'Opening...' : 'Manage Subscription'}
    </button>
  );
}

Check Before Opening Portal

async function openPortalIfEligible() {
  // First check if user has subscription
  const subResponse = await fetch('/api/billing/subscription', {
    headers: { 'Authorization': `Bearer ${apiKey}` }
  });

  const { subscription } = await subResponse.json();

  if (!subscription) {
    toast.error('You need an active subscription to access the billing portal');
    router.push('/pricing');
    return;
  }

  // Open portal
  const portalResponse = await fetch('/api/billing/create-portal-session', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${apiKey}` }
  });

  const { url } = await portalResponse.json();
  window.location.href = url;
}

Best Practices

Check Subscription First: Verify user has subscription before showing “Manage Billing” button.
Loading State: Show loading indicator while creating portal session.
Return URL Handling: Portal returns to /dashboard/billing. Ensure this page shows updated subscription data.
New Tab Option: Consider opening portal in new tab to prevent losing app state.
No Subscription = 404: Users who never subscribed will get 404 error. Handle this gracefully.
Portal URL Expires: Portal session URLs expire after a short time. Create new session each time.
Changes Apply Immediately: Subscription changes in portal take effect immediately via webhooks.

Integration Examples

Billing Settings Page

function BillingSettingsPage() {
  const [subscription, setSubscription] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    async function loadSubscription() {
      const response = await fetch('/api/billing/subscription', {
        headers: { 'Authorization': `Bearer ${apiKey}` }
      });

      const { subscription } = await response.json();
      setSubscription(subscription);
      setLoading(false);
    }

    loadSubscription();
  }, []);

  async function openPortal() {
    const response = await fetch('/api/billing/create-portal-session', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${apiKey}` }
    });

    const { url } = await response.json();
    window.location.href = url;
  }

  if (loading) return <div>Loading...</div>;

  return (
    <div className="billing-page">
      <h1>Billing & Subscription</h1>

      {subscription ? (
        <>
          <div className="subscription-info">
            <h2>Current Plan: {subscription.plan}</h2>
            <p>Status: {subscription.status}</p>
            <p>Next billing: {new Date(subscription.currentPeriodEnd).toLocaleDateString()}</p>
          </div>

          <button onClick={openPortal} className="manage-button">
            Manage Subscription
          </button>

          <p className="help-text">
            Update payment method, change plan, or cancel subscription
          </p>
        </>
      ) : (
        <>
          <p>No active subscription</p>
          <a href="/pricing">View Plans</a>
        </>
      )}
    </div>
  );
}

Portal Button with Confirmation

function BillingPortalButton({ actionType = 'manage' }) {
  const [opening, setOpening] = useState(false);

  async function handleClick() {
    const messages = {
      manage: 'Open billing portal to manage your subscription?',
      cancel: 'Open portal to cancel your subscription?',
      upgrade: 'Open portal to upgrade your plan?',
    };

    const confirmed = confirm(messages[actionType]);
    if (!confirmed) return;

    setOpening(true);
    try {
      const response = await fetch('/api/billing/create-portal-session', {
        method: 'POST',
        headers: { 'Authorization': `Bearer ${apiKey}` }
      });

      if (!response.ok) {
        toast.error('Unable to open portal');
        return;
      }

      const { url } = await response.json();
      window.location.href = url;
    } finally {
      setOpening(false);
    }
  }

  const buttonText = {
    manage: 'Manage Billing',
    cancel: 'Cancel Subscription',
    upgrade: 'Change Plan',
  };

  return (
    <button onClick={handleClick} disabled={opening}>
      {opening ? 'Opening...' : buttonText[actionType]}
    </button>
  );
}
function UserMenu() {
  const [subscription, setSubscription] = useState(null);

  useEffect(() => {
    async function loadSubscription() {
      const response = await fetch('/api/billing/subscription', {
        headers: { 'Authorization': `Bearer ${apiKey}` }
      });

      const { subscription } = await response.json();
      setSubscription(subscription);
    }

    loadSubscription();
  }, []);

  async function openPortal() {
    const response = await fetch('/api/billing/create-portal-session', {
      method: 'POST',
      headers: { 'Authorization': `Bearer ${apiKey}` }
    });

    const { url } = await response.json();
    window.location.href = url;
  }

  return (
    <DropdownMenu>
      <DropdownMenuItem onClick={() => router.push('/dashboard')}>
        Dashboard
      </DropdownMenuItem>

      <DropdownMenuItem onClick={() => router.push('/settings')}>
        Settings
      </DropdownMenuItem>

      {subscription && (
        <DropdownMenuItem onClick={openPortal}>
          Manage Billing
        </DropdownMenuItem>
      )}

      <DropdownMenuItem onClick={logout}>
        Sign Out
      </DropdownMenuItem>
    </DropdownMenu>
  );
}

Portal in New Tab

async function openPortalInNewTab() {
  const response = await fetch('/api/billing/create-portal-session', {
    method: 'POST',
    headers: { 'Authorization': `Bearer ${apiKey}` }
  });

  if (!response.ok) {
    toast.error('Failed to open billing portal');
    return;
  }

  const { url } = await response.json();

  // Open in new tab instead of redirect
  window.open(url, '_blank');

  toast.success('Billing portal opened in new tab');
}

Portal Return Handling

// In /dashboard/billing page
function BillingPage() {
  useEffect(() => {
    // Refresh subscription data when returning from portal
    async function refreshSubscription() {
      const response = await fetch('/api/billing/subscription', {
        headers: { 'Authorization': `Bearer ${apiKey}` }
      });

      const { subscription } = await response.json();

      // Update UI with latest subscription data
      setSubscription(subscription);

      // Show success message if recent change
      const recentlyUpdated = subscription &&
        (new Date() - new Date(subscription.updatedAt)) < 60000; // 1 minute

      if (recentlyUpdated) {
        toast.success('Subscription updated successfully');
      }
    }

    refreshSubscription();
  }, []);

  return <div>Billing Dashboard</div>;
}