Plugipay — BYO PayPal Provider Guide (/docs/providers/paypal) Copy

  • Project: plugipay (M4, Forjio split)
  • Source brief: spec/prd.md §10 · copy/landing-copy.md (voice anchor) · copy/docs/providers/xendit.md (sibling doc — structural parity) · copy/docs/providers/midtrans.md · copy/docs/providers/managed.md · copy/docs/quickstart-dev.md (voice + step substructure) · PayPal REST Apps / Webhooks / Sandbox Accounts reference docs
  • Audience: Developers or technical founders selling cross-border (USD, EUR, SGD, AUD, GBP — PayPal's supported receive currencies) who already own — or are willing to own — a PayPal Business account and want settlement in their name. Ecosystem norm: English.
  • Voice anchor: copy/landing-copy.md — "Stripe circa 2014, before corporate polish." Three voice rules: (1) admit trade-offs, (2) show with a runnable command, (3) no success theatre.
  • Register: Imperative mood. Terse. Numbers and exact paths before prose. No second-person marketing. No journey, seamlessly, empowering, unlock, best-in-class, revolutionary, next-generation, ecosystem solution.
  • Technical terms: full English. PayPal, REST App, Client ID, Secret, Webhook ID, PAYPAL-TRANSMISSION-SIG, PAYPAL-CERT-URL, PAYPAL-AUTH-ALGO, PAYPAL-TRANSMISSION-ID, PAYPAL-TRANSMISSION-TIME, sandbox, live, Business account, Personal account — keep verbatim. Entity names PascalCase: CheckoutSession, Ledger, PaymentToken.

Information architecture

# Block Goal
1 Hero Name the path, set the "10-minute connect" promise
2 Prerequisites PayPal Business account, Plugipay on Growth+, receive-currency note
3 Step 1 Create a REST App in the PayPal Developer Dashboard
4 Step 2 Paste Client ID + Secret into Plugipay
5 Step 3 Register Plugipay's webhook URL on the app
6 Step 4 Subscribe to the five events
7 Step 5 Sandbox test charge with a sandbox buyer account
8 Troubleshooting Signature, webhook delivery, sandbox/live, currency conversion
9 Placement notes Frontend route + component mapping

Five numbered steps, each with goal / actions / success checkpoint / pitfall.


1. Hero

Eyebrow

Providers · Bring your own PayPal

Headline (7 words)

Connect your PayPal account in ten minutes.

Subheadline (one sentence, 28 words)

Create one REST App, paste Client ID and Secret into Plugipay, register one webhook URL, subscribe to five events, and fire a sandbox charge — your PayPal contract stays yours.

Trade-off line (voice rule #1)

BYO PayPal means you own the relationship — settlement, KYC, pricing, disputes, chargeback scope. Plugipay doesn't take custody of funds. PayPal settles in 25 receive currencies (USD, EUR, SGD, AUD, GBP, JPY, etc.) — IDR is not one of them. For IDR or QRIS / VA / OVO, use Plugipay managed, BYO Xendit, or BYO Midtrans — same API, different rails.

What we'll subscribe to (right column, pinned on desktop)

PAYMENT.CAPTURE.COMPLETED
PAYMENT.CAPTURE.REFUNDED
BILLING.SUBSCRIPTION.ACTIVATED
BILLING.SUBSCRIPTION.CANCELLED
BILLING.SUBSCRIPTION.PAYMENT.FAILED

Primary CTAs

  • Start with step 1#step-1 (brand-700 button)
  • Compare with Plugipay managed/docs/providers/managed (text link)

2. Prerequisites

Before you start.

  • A PayPal Business account — not Personal. Sign up or upgrade at paypal.com/business. KYC takes 1–3 business days before live payouts clear; sandbox works immediately.
  • A Plugipay account on Growth tier or higher. BYO provider routing is gated on Starter; upgrade at Settings → Plan.
  • Ten minutes, two browser tabs (developer.paypal.com + Plugipay).

You do not need to write code here. Connection is dashboard-to-dashboard. Code (creating a CheckoutSession, handling plugipay.checkout_session.completed.v1) is covered in the developer quickstart. The PayPal Developer Dashboard ships with a pre-created sandbox Business + Personal account pair — use those in §7 before switching Plugipay's provider row to live. Currency details in §8 troubleshooting; short version: IDR sessions won't route to PayPal.


3. Step 1 — Create a REST App

Heading

Step 1 · Create a PayPal REST App

Goal

Create a sandbox REST App in the PayPal Developer Dashboard, copy the Client ID and Secret.

Actions

  1. Open developer.paypal.com and log in with your PayPal Business credentials. The Developer Dashboard is a separate surface from consumer paypal.com — apps, sandbox accounts, and webhooks all live here.
  2. Go to Apps & Credentials. The environment toggle (Sandbox / Live) sits top-right — leave it on Sandbox until §7 passes.
  3. Click Create App. Fill the form:
    • App Nameplugipay-sandbox or plugipay-live. Separate apps per environment makes revocation simple.
    • Type — select Merchant, not Platform. Platform apps require Partner onboarding, which Plugipay doesn't need for BYO.
    • Sandbox Business Account — leave the auto-selected default.
  4. Click Create App. The app detail page loads. Copy:
    • Client ID — starts with A (e.g. AXz9...). Visible permanently.
    • Secret — click Show on the Secret row, then Copy. Re-revealable, but save it to a password manager now.

[Screenshot: PayPal Developer Dashboard — Apps & Credentials (sandbox) → Create App form with name + Merchant type selected]

[Screenshot: PayPal Developer Dashboard — App detail page showing Client ID and Secret (Show) rows]

Success checkpoint

Two strings saved from the Sandbox tab: Client ID (A...) and Secret (E...), ~80 chars each. The app detail page URL contains ?env=sandbox — bookmark it for Step 3.

Pitfall — sandbox and live app registries are separate

PayPal maintains independent app registries per environment. Sandbox apps don't appear under Live — Client IDs, Secrets, and Webhook IDs are unrelated strings. When you ship live, repeat §3–§6 on the Live tab. Re-using sandbox credentials against api-m.paypal.com returns 401 invalid_client. Full live cutover details in §8 troubleshooting.


4. Step 2 — Paste credentials into Plugipay

Heading

Step 2 · Connect the app in Plugipay

Goal

Open the BYO PayPal connect form, paste Client ID + Secret, test the connection.

Actions

  1. In the Plugipay Dashboard, go to Settings → Providers → + Add provider → Bring your own PayPal.
  2. Fill the form:
    • Client ID — paste the Client ID from §3.
    • Secret — paste the Secret from §3.
    • Environment — pick Sandbox (radio). Keep it on sandbox until §7 passes.
  3. Click Test connection. Plugipay exchanges Client ID + Secret for an OAuth2 access token via POST /v1/oauth2/token against api-m.sandbox.paypal.com — a 200 with a bearer token proves auth. 401 invalid_client means wrong credentials or environment.
  4. Click Save & continue. Plugipay stores credentials encrypted at rest; the Secret is never returned in API responses or dashboard reads after save.

[Screenshot: Plugipay Dashboard — Settings → Providers → Bring your own PayPal connect form, "Test connection" success state]

Success checkpoint

The provider row shows PayPal · active · sandbox with the last 4 characters of the Client ID masked. Plugipay now holds a cached access token (1-hour TTL, auto-refreshed).

Pitfall — environment mismatch returns 401 invalid_client

Pasting sandbox credentials into Plugipay's Production radio (or vice versa) fails at Test connection with a generic 401 invalid_client — PayPal doesn't say "wrong environment." Verify by the app detail page URL: sandbox ends with ?env=sandbox, live ends with ?env=production. Match that to the Plugipay radio before Test connection.


5. Step 3 — Register Plugipay's webhook URL on the app

Heading

Step 3 · Point PayPal webhooks at Plugipay

Goal

Add Plugipay's ingress URL to the REST App's webhook list so PayPal delivers event notifications.

Actions

  1. In Plugipay, go to Settings → Providers → PayPal and copy the webhook URL:

    https://plugipay.com/webhooks/paypal/{merchant_id}
    

    Plugipay substitutes {merchant_id} automatically — copy the rendered URL, not the template.

  2. In the PayPal Developer Dashboard, open the app from §3. Scroll to the Webhooks section at the bottom of the app detail page.

  3. Click Add Webhook. Paste the URL into the Webhook URL field. Don't check Event types: All events — that floods your ingress with ~40 types Plugipay doesn't consume. Save.

  4. PayPal generates a Webhook ID (WH-...) on save. Copy it and paste into Plugipay under Settings → Providers → PayPal → Webhook ID. Plugipay calls PayPal's POST /v1/notifications/verify-webhook-signature with this ID — without it, every inbound webhook fails verification.

[Screenshot: PayPal Developer Dashboard — App detail → Webhooks → Add Webhook form with Plugipay URL pasted]

[Screenshot: Plugipay Dashboard — Settings → Providers → PayPal → Webhook ID field with WH-... pasted]

Success checkpoint

PayPal's app detail page lists Plugipay's URL with an active Webhook ID. Plugipay's provider row shows webhook configured with the Webhook ID masked to its last 4 characters.

Pitfall — missing or wrong Webhook ID breaks signature verification

Unlike Xendit (header token) or Midtrans (SHA512 hash), PayPal verification requires a round-trip API call keyed on Webhook ID. Blank or cross-app Webhook ID means every inbound webhook returns 401 signature_invalid and PayPal retries for 25 hours before giving up. Verify the Plugipay field matches the app detail page character for character.


6. Step 4 — Subscribe to the events Plugipay consumes

Heading

Step 4 · Enable five webhook events

Goal

Tell PayPal which event types to deliver to Plugipay's ingress URL.

Actions

On the webhook you added in §5, click Edit and enable exactly these five event types:

PAYMENT.CAPTURE.COMPLETED
PAYMENT.CAPTURE.REFUNDED
BILLING.SUBSCRIPTION.ACTIVATED
BILLING.SUBSCRIPTION.CANCELLED
BILLING.SUBSCRIPTION.PAYMENT.FAILED

Save.

Two things to watch for:

  1. All five must be enabled. Skipping PAYMENT.CAPTURE.COMPLETED means one-time charges never reach completed in Plugipay. Skipping the BILLING.SUBSCRIPTION.* trio breaks the recurring billing lifecycle — subscriptions activate in PayPal but stay pending in Plugipay.
  2. Extras are noisy but harmless. Plugipay ignores unknown types; they still clutter PayPal's delivery log and count against your webhook rate limits. Subscribe only to the five unless you're debugging.

[Screenshot: PayPal Developer Dashboard — App detail → Webhooks → Edit event selection with the five Plugipay events checked]

Success checkpoint

The webhook row shows 5 event types matching the list above. The event count on the app detail page reads 5 of 40+.

Pitfall — PAYMENT.SALE.COMPLETED is the legacy event

PayPal's older v1 Payments API emits PAYMENT.SALE.COMPLETED; the v2 Orders API (which Plugipay uses) emits PAYMENT.CAPTURE.COMPLETED. Both show up in the event picker. Subscribing to the legacy SALE variant delivers nothing because Plugipay doesn't create v1 Payments — charges stay open indefinitely. Subscribe to CAPTURE, not SALE.


7. Step 5 — Sandbox test charge

Heading

Step 5 · Fire a test charge end to end

Goal

Create a CheckoutSession via CLI, complete a PayPal payment in the sandbox with a sandbox buyer account, watch the event reach Plugipay.

Actions

First, grab sandbox Personal (buyer) account credentials:

  1. Go to Testing Tools → Sandbox Accounts in the PayPal Developer Dashboard. Two accounts are auto-created: one Business (merchant), one Personal (buyer).
  2. Click the Personal row → View/Edit Account. Copy the Email (e.g. sb-buyer-12345@personal.example.com) and System-Generated Password.

Terminal 1 — create a session:

plugipay checkout create \
  --amount 1000 \
  --currency USD \
  --methods paypal \
  --success-url https://example.com/thanks \
  --cancel-url https://example.com/cancel \
  --mode test \
  --provider paypal \
  --json

Response:

{ "id": "cs_01HYC...", "hostedUrl": "https://plugipay.com/c/cs_01HYC...", "status": "open" }

Terminal 2 — listen for the completion event:

plugipay events listen --types plugipay.checkout_session.completed.v1 --mode test

Open hostedUrl in a browser, click PayPal. PayPal's hosted checkout opens in a popup. Log in with the sandbox Personal email + password from above. Confirm the payment.

Success checkpoint

Terminal 2 prints a plugipay.checkout_session.completed.v1 event with data.object.adapter = "paypal" and status = "completed". In the PayPal Developer Dashboard under Sandbox Accounts → Business account → View transactions, the matching capture shows Completed. In Plugipay under Events → Deliveries, the inbound PAYMENT.CAPTURE.COMPLETED shows 200 and the outbound plugipay.checkout_session.completed.v1 shows 200.

Pitfall — sandbox login goes to sandbox.paypal.com with a generated password

The popup from hostedUrl during test goes to https://www.sandbox.paypal.com — real PayPal credentials won't work, only the sandbox Personal account from Testing Tools → Sandbox Accounts. Also: PayPal regenerates the system-generated password every time you open View/Edit Account with Change Password toggled on, so copy the current one each time or set a custom password once (PayPal honors those across regenerations).


8. Troubleshooting

Pitfall — invalid signature / webhook verification failed

PayPal signs every webhook with the app's RSA key pair; Plugipay verifies via POST /v1/notifications/verify-webhook-signature with five headers (PAYPAL-AUTH-ALGO, PAYPAL-CERT-URL, PAYPAL-TRANSMISSION-ID, PAYPAL-TRANSMISSION-SIG, PAYPAL-TRANSMISSION-TIME), the Webhook ID, and the raw body. A FAILURE response produces 401 signature_invalid in Plugipay. Usually a stale Webhook ID (you deleted and recreated the webhook in PayPal) or an environment mismatch. Re-copy the Webhook ID from App detail → Webhooks and update via Settings → Providers → PayPal → Edit.

Pitfall — webhook not received

Check App detail → Webhooks → Webhook Events in the PayPal Developer Dashboard for delivery history — click any row to see the request/response and retry counts. Non-2xx means Plugipay rejected (check signature and environment). No row at all means §6 subscriptions are missing or the Webhook URL in §5 points somewhere else. In Plugipay, Events → Deliveries (direction inbound) confirms receipts. PayPal retries on non-2xx with exponential backoff for 25 hours across ~25 attempts — transient Plugipay downtime self-heals.

Pitfall — sandbox vs live mismatch

Second-most-common support ticket after signature errors. PayPal apps, Client IDs, Secrets, Webhook IDs, and sandbox accounts are separate between sandbox (api-m.sandbox.paypal.com) and live (api-m.paypal.com). Flipping Plugipay to live means repeating §3–§6 on the Live tab — new REST App, new Client ID + Secret, new webhook, new Webhook ID, same five events, Plugipay provider row set to live. Miss one and live charges silently stay open. The app detail page URL must end with ?env=production for live.

Pitfall — currency conversion gotchas (IDR especially)

PayPal does not settle in IDR. CheckoutSession with currency: "IDR" routed to PayPal fails at session creation with 400 unsupported_currency. Two options: (a) denominate in USD or another supported receive currency — buyers pay FX spread on their end; (b) let Plugipay's router fall back to Xendit/Midtrans for IDR and reserve PayPal for USD/EUR/cross-border. Also watch PayPal's asymmetric conversion fee (~3.5–4% above mid-market) when captured amounts convert to your Business account's primary holding currency.

If your error isn't listed, email support@forjio.com with your Account ID (acc_...) and the step you hit. We don't send canned answers.


9. Placement notes

All sections render in code/frontend/src/app/(marketing)/docs/providers/paypal/page.tsx as scroll-anchored blocks (#hero, #prerequisites, #step-1#step-5, #troubleshooting). Sidebar nav from docs/index.md §3.3 Providers. Screenshot assets → code/frontend/public/docs/providers/paypal/ (PNG, 2x) — Iro/Hikari during implementation. Shared primitives (code-block.tsx, callout.tsx, step-badge.tsx) reused from /docs/quickstart-dev and /docs/providers/xendit. Cross-page links: /docs/providers/managed (shipped), /docs/providers/xendit (shipped), /docs/providers/midtrans (shipped), /docs/quickstart-dev, /docs/pricing, external https://developer.paypal.com, https://paypal.com/business, mailto:support@forjio.com.


End of docs/providers/paypal.md.

Plugipay — Payments that don't tax your success