PayPal Integration Guide
Connect your PayPal account to ExisOne to automatically generate and email license keys when customers purchase on your website.
1. Create a PayPal REST App
- Log into PayPal Developer Dashboard.
- Go to Apps & Credentials.
- Select Sandbox for testing (switch to Live later for production).
- Click Create App and give it a name.
- Copy your Client ID and Secret (click "Show" to reveal).
2. Configure PayPal Webhook
- In your PayPal app, scroll down to Webhooks and click Add Webhook.
- Set the Webhook URL to:
https://www.exisone.com/api/paypal/webhook - Select these events:
PAYMENT.CAPTURE.COMPLETED- Triggers license generation for one-time paymentsBILLING.SUBSCRIPTION.ACTIVATED- Triggers license generation for new subscriptionsPAYMENT.SALE.COMPLETED- Triggers license generation for subscription renewals
- Save the webhook and copy the Webhook ID (shown in the webhook list).
3. Register Your PayPal App in ExisOne
- In ExisOne, go to PayPal Integration > PayPal Apps and Events.
- Click New App and enter:
- Webhook ID - from step 2
- Client ID - from step 1
- Client Secret - from step 1
- Set Is Live to match your credentials (unchecked for Sandbox, checked for Live).
- Check Active and Default.
- Save the app.
ExisOne uses the Webhook ID to match incoming webhooks to your account.
4. Configure Your Product in ExisOne
For each product you sell, configure these settings on the Products page:
- Auto Email: Check this box to automatically email license keys after purchase.
- Default License Days: Set to 0 for Perpetual, or a number of days for time-limited licenses.
- Email Template (optional): Customize the email sent to customers.
4b. Configure Subscription Plan Mappings (Optional)
If you want to sell subscription-based licenses, you can map your ExisOne products to PayPal Plan IDs:
- In PayPal Dashboard, go to Products & Plans and create a subscription plan.
- Copy the Plan ID (starts with
P-...). - In ExisOne, go to PayPal Apps and Events.
- Scroll to Subscription Plan Mappings.
- Click Save after entering:
- PayPal App - Select your PayPal app
- Product - Select your ExisOne product
- PayPal Plan ID - Paste the Plan ID from PayPal
- Display Name (optional) - e.g., "Monthly", "Annual"
- Display Price (optional) - e.g., 9.99
5. Find Your Product ID
You'll need your Product ID to include in PayPal orders. Find it on the Products page - it's displayed in the ID column.
42, you'll use "custom_id": "42" in your PayPal order.
6. How the Payment Flow Works
Your Website PayPal ExisOne
| | |
|-- Create order --------------->| |
| (using PayPal API directly) | |
| custom_id: "42" | |
| | |
| Customer approves payment | |
| | |
|-- Capture order -------------->| |
| | |
| |-- Webhook ------------------ >|
| | PAYMENT.CAPTURE.COMPLETED |
| | |
| | Match app by Webhook ID --->|
| | Extract productId from |
| | custom_id or item sku |
| | |
| | Generate license key |
| | |
| | Email key to customer |
| | |
7. Integrate PayPal on Your Website
Use PayPal's API directly on your website. The key is to include your ExisOne productId in the order's custom_id field or item sku.
Server-Side: Create Order (Node.js Example)
const paypal = require('@paypal/checkout-server-sdk');
// Configure PayPal environment
const environment = new paypal.core.SandboxEnvironment(
'YOUR_CLIENT_ID',
'YOUR_CLIENT_SECRET'
);
const client = new paypal.core.PayPalHttpClient(environment);
app.post('/create-paypal-order', async (req, res) => {
const request = new paypal.orders.OrdersCreateRequest();
request.prefer("return=representation");
request.requestBody({
intent: 'CAPTURE',
purchase_units: [{
custom_id: '42', // Your ExisOne Product ID - REQUIRED
amount: {
currency_code: 'USD',
value: '29.99',
breakdown: {
item_total: { currency_code: 'USD', value: '29.99' }
}
},
items: [{
name: 'Pro License',
sku: '42', // Alternative: Product ID in SKU
quantity: '1',
unit_amount: { currency_code: 'USD', value: '29.99' }
}]
}]
});
const order = await client.execute(request);
res.json({ id: order.result.id });
});
app.post('/capture-paypal-order', async (req, res) => {
const { orderID } = req.body;
const request = new paypal.orders.OrdersCaptureRequest(orderID);
const capture = await client.execute(request);
res.json({ status: capture.result.status });
});
Client-Side: PayPal Buttons (JavaScript)
<div id="paypal-button-container"></div>
<script src="https://www.paypal.com/sdk/js?client-id=YOUR_CLIENT_ID"></script>
<script>
paypal.Buttons({
createOrder: async () => {
const response = await fetch('/create-paypal-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
const order = await response.json();
return order.id;
},
onApprove: async (data) => {
const response = await fetch('/capture-paypal-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ orderID: data.orderID })
});
const result = await response.json();
if (result.status === 'COMPLETED') {
alert('Payment successful! Check your email for your license key.');
}
}
}).render('#paypal-button-container');
</script>
custom_id field (or item sku) must contain your ExisOne Product ID. Without it, ExisOne cannot determine which product to generate a license for.
Where to Put the Product ID (One-Time Payments)
| Field | Location | Description |
|---|---|---|
custom_id | purchase_units[0].custom_id | Preferred method - ExisOne Product ID |
sku | purchase_units[0].items[0].sku | Alternative - ExisOne Product ID in item SKU |
quantity | purchase_units[0].items[0].quantity | Number of licenses to generate |
7b. Subscription Licenses
For subscription-based licensing (e.g., monthly or annual renewals), ExisOne issues a license key on the initial BILLING.SUBSCRIPTION.ACTIVATED event and then extends the same key's expiration date on every renewal PAYMENT.SALE.COMPLETED event. The subscriber never has to swap activation keys.
ExpirationDate is bumped by the plan's DurationDays (or the product's DefaultLicenseDays if no plan is specified). Any unused time from the prior period is preserved — the new expiration is max(now, existingExpiration) + days.
custom_id when creating the subscription. This is where ExisOne reads the Product ID from. Format: "productId" or "productId:quantity" for multiple licenses. The PayPal subscription.id is then stored on each issued license so renewals can find it.
BILLING.SUBSCRIPTION.ACTIVATED and PAYMENT.SALE.COMPLETED for the initial payment. ExisOne treats the second event as a duplicate of the first when the most recent license tagged with this subscription.id is younger than half the billing period; this prevents the initial period from being extended twice. Subsequent renewals always pass the threshold and extend normally.
PayPalSubscriptionId stored on the license) automatically fall back to the old behavior — a fresh key is minted on each renewal. Existing customers are not broken.
Subscription Loyalty: Convert to Perpetual After N Payments
On the product configuration page, the Convert to Perpetual After N Payments setting lets you reward long-term subscribers. Set the threshold (e.g. 6) and after that many successful payments the customer's license becomes permanent — the activation key never expires and the PayPal subscription is automatically cancelled so they aren't billed again.
- The initial purchase counts as payment #1. With threshold = 6, the customer pays 5 renewals after signup; the 6th payment is the one that flips the license to perpetual.
- Failed payments do not advance the counter, but they also do not reset it — a recovered card simply resumes progress.
- At the conversion point ExisOne calls
POST /v1/billing/subscriptions/{id}/cancelvia the PayPal REST API so no furtherPAYMENT.SALE.COMPLETEDevents fire, sends the customer a "Congratulations — your license is now permanent" email, and fires thelicense.perpetual_unlockedwebhook to any of your endpoints subscribed to that event. - Once
IsPerpetualis true the license'sExpirationDateis set to9999-12-31; validation logic treats this the same as any non-expired license.
PayPal Subscription Setup
- In PayPal, create a Product under Products & Plans
- Create a Billing Plan for that product (e.g., $99/year)
- When creating subscriptions, include
custom_idwith your ExisOne Product ID
Server-Side: Create Subscription (Node.js Example)
const paypal = require('@paypal/checkout-server-sdk');
app.post('/create-subscription', async (req, res) => {
const request = new paypal.subscriptions.SubscriptionsCreateRequest();
request.requestBody({
plan_id: 'P-XXXXXXXXXXXXXXXXXXXXXXXX', // Your PayPal Plan ID
custom_id: '42', // ExisOne Product ID - REQUIRED
// Or for multiple licenses: '42:5' (productId:quantity)
subscriber: {
email_address: req.body.email
},
application_context: {
return_url: 'https://yoursite.com/success',
cancel_url: 'https://yoursite.com/cancel'
}
});
const subscription = await client.execute(request);
res.json({ id: subscription.result.id, approvalUrl: subscription.result.links.find(l => l.rel === 'approve').href });
});
How Subscription Licensing Works
Initial Subscription Renewals (Monthly / Annual)
| |
|-- BILLING.SUBSCRIPTION.ACTIVATED --> |-- PAYMENT.SALE.COMPLETED ------->
| |
| ExisOne fetches subscription from | ExisOne looks up license by
| PayPal API, reads custom_id | PayPalSubscriptionId
| |
| Generates license key(s) | Extends ExpirationDate on the
| Tags each with subscription.id | SAME key (no new key issued)
| Emails to subscriber | Emails renewal confirmation
| |
Where to Put the Product ID (Subscriptions)
| Field | Format | Description |
|---|---|---|
custom_id | "42" | ExisOne Product ID only (1 license) |
custom_id | "42:5" | ExisOne Product ID with quantity (5 licenses) |
8. Test Your Integration
- Make sure you're using Sandbox credentials.
- Trigger a purchase from your website.
- Log into PayPal Sandbox with a test buyer account to complete payment.
- Check PayPal Apps and Events in ExisOne - you should see the webhook event.
- Verify the license key appears under License Keys in the ExisOne dashboard.
- Confirm the customer received the license key email (if Auto Email is enabled).
You can also use ExisOne's built-in Test Purchase page to verify webhook connectivity before integrating with your website.
PayPal Sandbox Test Accounts
In PayPal Developer Dashboard, go to Sandbox > Accounts to find or create test buyer/seller accounts for testing.
9. Notes
- One-time payments: The
custom_idor itemskumust contain your ExisOne Product ID. - Subscriptions: The
custom_idon the subscription holds the Product ID. Format:"42"or"42:5"for quantity. - Quantity is read from the item's
quantityfield, subscriptioncustom_id, plan name, or calculated from total amount / product price. - Perpetual licenses (Default License Days = 0) never expire.
- Emails use the product's template (placeholders:
{{ActivationKey}},{{ApiKey}},{{ApiQuota}},{{ProductName}},{{PlanName}},{{CustomerEmail}},{{PaymentProcessorCustomerName}},{{CustomerPortalUrl}},{{ExpirationDate}}). On renewal,{{ExpirationDate}}resolves to the new expiry after extension.
Troubleshooting
Webhook not received
- Verify the webhook URL is exactly
https://www.exisone.com/api/paypal/webhook - Check that the webhook is enabled and listening for the required events:
PAYMENT.CAPTURE.COMPLETEDfor one-time paymentsBILLING.SUBSCRIPTION.ACTIVATEDandPAYMENT.SALE.COMPLETEDfor subscriptions
- In PayPal Developer Dashboard, go to Webhooks and check the event delivery log
License not generated (one-time payments)
- Verify your PayPal app is registered in ExisOne with the correct Webhook ID
- Check that the order includes
custom_idor itemskuwith a valid Product ID - Ensure the product exists in ExisOne and has Auto Email enabled
- Check the PayPal Events list for error details
Subscription renewals not extending the license
- The license row in EO_Licenses must have
PayPalSubscriptionIdpopulated. This is set on the initialBILLING.SUBSCRIPTION.ACTIVATEDevent — subscriptions created before that column existed will not have it and will fall back to issuing a new key on renewal. Backfill manually if needed:UPDATE EO_Licenses SET PayPalSubscriptionId = 'I-XXXXXXXXXXXX' WHERE ActivationKey = 'KEY-...'; - Ensure the subscription has a
custom_idset with the ExisOne Product ID. Thecustom_idmust be set when creating the subscription — it cannot be added later. - Check that your webhook is listening for both
BILLING.SUBSCRIPTION.ACTIVATEDandPAYMENT.SALE.COMPLETEDevents. - Verify the PayPal app credentials (Client ID/Secret) are correct for API calls to fetch subscription details.
- If a renewal arrived but no extension happened, check the application log for "treating as duplicate of initial activation, skipping" — that means the license was tagged less than half a billing period ago, so the event was treated as the second leg of the initial purchase.
- View the event in PayPal Events to check for errors.
Event log
- View events in ExisOne at PayPal Integration > PayPal Apps and Events
- Click on an event to see the raw JSON payload for debugging
10. Setup Checklist
Before going live, verify all items are complete:
| Step | Where | Status |
|---|---|---|
| PayPal REST app created | PayPal Developer Dashboard | |
| Webhook created pointing to ExisOne | PayPal Developer Dashboard | |
| PayPal app registered with Webhook ID and credentials | ExisOne > PayPal Apps | |
| Product created in ExisOne | ExisOne > Products | |
| Product has Auto Email enabled | ExisOne > Products | |
| Email template configured | ExisOne > Products > Email Template | |
| Subscription plan mappings configured (if using subscriptions) | ExisOne > PayPal Apps > Plan Mappings | |
| Website integration includes productId in custom_id or sku | Your website code | |
| Test purchase completed successfully (Sandbox) | Your website | |
| Webhook received in ExisOne | ExisOne > PayPal Events | |
| License key generated and emailed | ExisOne > License Keys |
11. Go Live
- In PayPal Developer Dashboard, switch to Live mode.
- Create a new webhook pointing to
https://www.exisone.com/api/paypal/webhookwith live credentials. - In ExisOne, add a new PayPal app with your Live credentials and Webhook ID. Check Is Live and Active.
- Update your website to use your Live Client ID.
- Make a real purchase (you can refund it) to verify the complete flow works in production.