Skip to content
ESC

Searching...

Quick Links

Type to search • Press to navigate • Enter to select

Keep typing to search...

No results found

No documentation matches ""

PayPal Billing.

Set up PayPal subscription billing for partners, step by step.

Jun 22, 2026

This is the full setup guide for PayPal. For the billing overview, the plan tiers, and the other modes, see Billing Configuration.

What this mode does

PayPal runs your partner subscriptions through the PayPal REST Subscriptions API. A partner approves the subscription on PayPal, returns to the app, and the app captures it and sets the plan. From then on, a webhook keeps the plan in step with PayPal.

No hosted portal. PayPal has no equivalent of Stripe's hosted billing portal. Partners cancel and change plans on in-app screens on the My Plan page. A plan change is a cancel followed by a new subscription, because PayPal cannot revise a plan in place. See What partners see on My Plan.

When to use it

Use PayPal when your partners prefer to pay with PayPal. PayPal handles the approval and the payments, the app captures the result and keeps each partner's plan in step, and partners manage their subscription from in-app screens.

Before you start

⚠️ Keep sandbox and live separate. PayPal keeps sandbox and live apart across the app credentials, the billing plans, and the webhook. PAYPAL_MODE selects which set the app talks to, so everything you create has to live in the same environment. Build and confirm in sandbox first, then repeat in live. Never mix a sandbox value with a live value.

Step 1: Create a REST app and copy the credentials

  1. Sign in to the PayPal Developer Dashboard with your PayPal business account.
  2. Set the Sandbox / Live toggle to the environment you are configuring. It must match your planned PAYPAL_MODE.
  3. Open Apps & Credentials, find the REST API apps section, and click Create App (or open an app you already made).
  4. Copy the Client ID, and next to Secret, click to reveal and copy the value.

Sandbox and live have separate apps and separate credentials. The pair you copy has to come from the environment that matches PAYPAL_MODE. See PayPal's REST API credentials for more.

Step 2: Create one plan per paid tier and copy the Plan IDs

You need a PayPal billing plan for each paid tier and interval you sell, for example Silver monthly, Silver yearly, Gold monthly. A plan ID looks like P-1AB23456CD789012EF34GHIJ.

Without code (PayPal business account). Sign in to your PayPal business account and go to Pay & Get Paid → Accept Payments → Subscriptions. Open Subscription plans and click Create Plan. Set the price and the billing cycle (monthly or yearly), then turn the plan on. To read the Plan ID back, open your plans list at paypal.com/billing/plans (use sandbox.paypal.com/billing/plans while testing). Each plan shows its P-… ID.

With the API. Get an access token, create one catalog product, then create one plan per tier and interval. Use the live host api-m.paypal.com when you go live.

# 1. Get an access token
curl -s https://api-m.sandbox.paypal.com/v1/oauth2/token \
  -u "PAYPAL_CLIENT_ID:PAYPAL_SECRET" \
  -d "grant_type=client_credentials"

# 2. Create a product once. The response holds an id like PROD-XXXXXXXX
curl -s -X POST https://api-m.sandbox.paypal.com/v1/catalogs/products \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Reward Loyalty","type":"SERVICE","category":"SOFTWARE"}'

# 3. Create a plan. The response holds the Plan ID, e.g. P-XXXXXXXX
curl -s -X POST https://api-m.sandbox.paypal.com/v1/billing/plans \
  -H "Authorization: Bearer ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "product_id": "PROD-XXXXXXXX",
        "name": "Gold (monthly)",
        "billing_cycles": [ /* pricing + frequency, see the link below */ ]
      }'

The id in the create-plan response (the P-… value) is what you map in .env. For the full billing_cycles shape, see PayPal's Create plan reference.

Step 3: Add the credentials and plan IDs to .env

# Billing provider
BILLING_PROVIDER=paypal

# PayPal REST app credentials (Apps & Credentials)
PAYPAL_CLIENT_ID=...
PAYPAL_SECRET=...

# sandbox (default) or live: must match where you created the app, plans, and webhook
PAYPAL_MODE=sandbox

# Webhook id (you get this in Step 4)
PAYPAL_WEBHOOK_ID=...

# Plan IDs: one monthly and one yearly per paid tier.
# Tier 1 is the free tier, so it needs no plan ID.
PAYPAL_PLAN_TIER2_MONTHLY=P-...
PAYPAL_PLAN_TIER2_YEARLY=P-...
PAYPAL_PLAN_TIER3_MONTHLY=P-...
PAYPAL_PLAN_TIER3_YEARLY=P-...
PAYPAL_PLAN_TIER4_MONTHLY=P-...
PAYPAL_PLAN_TIER4_YEARLY=P-...

⚠️ Three values are required: PAYPAL_CLIENT_ID, PAYPAL_SECRET, and PAYPAL_WEBHOOK_ID. The provider stays unconfigured until all three are set. PAYPAL_MODE is optional and defaults to sandbox; set PAYPAL_MODE=live when you move to live. Without the webhook id, the app cannot verify signatures, so the webhook endpoint rejects every request with 403 Forbidden.

The credentials live in config/services.php under the paypal key. The plan IDs load through config/default.php, so they keep working with php artisan config:cache.

Step 4: Create the webhook and copy the Webhook ID

Your webhook endpoint is:

https://your-domain.com/api/paypal/webhook
  1. In the PayPal Developer Dashboard, set the Sandbox / Live toggle to match PAYPAL_MODE.
  2. Open Apps & Credentials and select your app.
  3. Scroll to the Webhooks section and click Add Webhook.
  4. In Webhook URL, enter https://your-domain.com/api/paypal/webhook.
  5. Under Event types, select these (or pick All events):
    • BILLING.SUBSCRIPTION.ACTIVATED
    • BILLING.SUBSCRIPTION.UPDATED
    • BILLING.SUBSCRIPTION.CANCELLED
    • BILLING.SUBSCRIPTION.SUSPENDED
    • BILLING.SUBSCRIPTION.EXPIRED
    • PAYMENT.SALE.COMPLETED (renewal payments)
  6. Click Save.

PayPal now lists the webhook with an ID that looks like WH-1AB23456CD789012E-3FG45678HI901234J. Copy that Webhook ID into .env as PAYPAL_WEBHOOK_ID. Sandbox and live each have their own app and their own webhook, so create the webhook under the same environment as your credentials.

Step 5: Confirm

Open the Health Center. The Billing Provider check reads PayPal in green once the credentials and the webhook id are set, and its detail line shows whether you are in sandbox or live. To confirm delivery, open the webhook in the PayPal dashboard and review recent deliveries: a healthy delivery returns 200. A 403 means PAYPAL_WEBHOOK_ID is missing or wrong (see Troubleshooting).

Test before you go live

  1. With sandbox credentials, sandbox plan IDs, and a sandbox webhook in place, register a test partner and subscribe with a sandbox buyer account.
  2. Confirm the partner's My Plan page shows the new tier and an Active badge.
  3. When the flow works end to end, repeat Steps 1 to 4 in live: create a live app, create live plans, and create a live webhook. Set PAYPAL_MODE=live and put the live values in .env.

What partners see on My Plan

PayPal mode shows the same status badge, plan comparison, and monthly/yearly toggle as the other paid mode. Self-service differs, because there is no hosted portal:

  • Without an active subscription, partners see Upgrade, which starts the PayPal approval redirect.
  • With an active subscription, partners see Change Plan, which opens a confirmation that explains the cancel-and-resubscribe step, plus a Cancel Subscription control with a two-step confirm.
  • The "Manage Billing" portal button stays hidden.

Admin-created and legacy partners (those with a created_by value) see the plan cards but no self-service buttons. They contact their administrator for changes.

How the two in-app controls work:

  • Cancel. A two-step confirm cancels the PayPal subscription. Access restricts once PayPal reports it cancelled. The partner can subscribe again at any time.
  • Change plan. PayPal cannot revise a plan in place, so changing cancels the current subscription and starts a new one. The partner confirms, the app cancels the current subscription first so they are never billed twice, then sends them to PayPal to approve the new plan. If they leave without approving, they have no active subscription and can subscribe again.

Subscription states

The provider maps PayPal status onto the shared entitlement vocabulary (see the state reference):

PayPal status Entitlement state Access
(no subscription) legacy Full plan access (pre-billing)
ACTIVE active Full plan access
APPROVAL_PENDING, APPROVED incomplete Restricted until activation finishes
SUSPENDED suspended Restricted
CANCELLED, EXPIRED cancelled Restricted; plan label kept
(unknown) suspended Restricted, as a fail-safe

legacy never restricts access. The mapping is fail-safe: an unrecognized status restricts access rather than granting it.

Webhook synchronization safety

PayPal cannot use Laravel Cashier, so the handler is hand-rolled and hardened for correctness:

  • Verifies the signature first. It checks the transmission id, signature, certificate URL, and timestamp with PayPal before it trusts the event.
  • Reads the live subscription. Rather than trusting the status in the payload, the handler fetches the live subscription from PayPal and syncs from that. Re-running the same event reaches the same result, and a late or out-of-order event still resolves to the current truth.
  • Processes each event once. It keys on the PayPal event id to skip duplicates.
  • Active-subscription guard. It moves the plan only for an ACTIVE subscription. Cancelled, suspended, expired, and approval-pending states save the status so access restricts, but they never grant a paid tier and never drop the plan label.
  • Strict plan-ID mapping. It reverse-maps the live plan id against config/default.php to find the tier. It ignores an unknown plan id.
  • Ties back to the partner. The subscription's custom_id carries the partner id set at checkout, so the handler finds the partner without trusting any session.

Troubleshooting

Symptom Likely cause Resolution
Webhook deliveries return 403 PAYPAL_WEBHOOK_ID is missing, or it does not match the webhook sending the events Set PAYPAL_WEBHOOK_ID to the id of the exact webhook in the dashboard. The Health Center shows PayPal (webhook insecure) when it is missing.
Webhook returns 200 but the plan does not change The event was for a non-ACTIVE state, the plan id is not mapped, or custom_id did not match a partner Confirm the PAYPAL_PLAN_* mapping matches the live plan id, and that the subscription started inside the app, so custom_id holds the partner id.
Partner stuck on APPROVAL_PENDING The buyer approved but PayPal has not activated yet, or the buyer left the approval page PayPal activates within moments, and the ACTIVATED webhook syncs it. If the buyer left, they subscribe again. No state is corrupted.
Partner asks why "Change Plan" cancels their subscription Expected. PayPal cannot revise a plan in place A plan change is a cancel then a new subscription. The confirmation dialog says so, and the app cancels the old subscription first, so there is no double charge.
Tokens or webhooks rejected after switching environments PAYPAL_MODE does not match where you created the credentials, plans, and webhook Keep PAYPAL_MODE, the app credentials, the P-… plan ids, and the webhook in one environment. The Health Center detail line shows the active mode.
You need to manage a partner's subscription There is no admin-side PayPal portal The partner cancels or changes from My Plan, or you act on the subscription in the PayPal dashboard. The next webhook, or the partner reopening My Plan, re-syncs local state.

Quick reference

# === PayPal (.env) ===

BILLING_PROVIDER=paypal

# Credentials: Apps & Credentials (match the Sandbox/Live toggle)
PAYPAL_CLIENT_ID=...
PAYPAL_SECRET=...
PAYPAL_MODE=sandbox          # or "live"
PAYPAL_WEBHOOK_ID=WH-...      # the app's Webhooks section, after Add Webhook

# Plan IDs: Subscriptions → Subscription plans, or the billing/plans API (P-...)
PAYPAL_PLAN_TIER2_MONTHLY=P-...
PAYPAL_PLAN_TIER2_YEARLY=P-...
PAYPAL_PLAN_TIER3_MONTHLY=P-...
PAYPAL_PLAN_TIER3_YEARLY=P-...
PAYPAL_PLAN_TIER4_MONTHLY=P-...
PAYPAL_PLAN_TIER4_YEARLY=P-...