Python SDK for PayPlus payment gateway with subscription management for SaaS apps
Project description
PayPlus Python SDK (Unofficial)
Note: This is an unofficial SDK and is not affiliated with or endorsed by PayPlus.
A Python SDK for PayPlus payment gateway with built-in subscription management for SaaS applications.
Features
- Full PayPlus API coverage — payment pages, transactions, recurring payments, customers
- Subscription management — payment-link-based recurring billing for SaaS apps
- Database integration — MongoDB and SQLAlchemy storage backends
- Webhook handling — IPN/webhook integration with HMAC signature verification
- Async support — full async/await for modern Python apps
- Type safe — Pydantic models with full type hints
Installation
pip install payplus-python
With optional dependencies:
pip install payplus-python[fastapi] # FastAPI webhook integration
pip install payplus-python[postgres] # PostgreSQL storage
pip install payplus-python[mongodb] # MongoDB storage
Implementation Steps
A step-by-step guide covering the full subscription lifecycle in your app.
Step 1: Initialize the SDK
from decimal import Decimal
from payplus import PayPlus, SubscriptionManager
from payplus.models.subscription import BillingCycle
from payplus.subscriptions.storage import MongoDBStorage
from payplus.webhooks import WebhookHandler
from motor.motor_asyncio import AsyncIOMotorClient
client = PayPlus(
api_key="your_api_key",
secret_key="your_secret_key",
sandbox=True,
)
mongo = AsyncIOMotorClient("mongodb://localhost:27017")
storage = MongoDBStorage(mongo.your_database)
manager = SubscriptionManager(client, storage)
webhook_handler = WebhookHandler(client)
Step 2: Define your plans (run once on app setup)
await manager.create_tier(
tier_id="basic",
name="Basic",
price=Decimal("29"),
billing_cycle=BillingCycle.MONTHLY,
trial_days=7,
)
await manager.create_tier(
tier_id="pro",
name="Pro",
price=Decimal("79"),
billing_cycle=BillingCycle.MONTHLY,
trial_days=14,
)
Step 3: User signs up
customer = await manager.create_customer(
email="user@example.com",
name="John Doe",
phone="050-1234567",
)
# Save customer.id in your user record
Step 4: User subscribes to a plan
subscription = await manager.create_subscription(
customer_id=customer.id,
tier_id="pro",
callback_url="https://yourapp.com/webhooks/payplus",
success_url="https://yourapp.com/subscription/success",
failure_url="https://yourapp.com/subscription/failure",
)
# Redirect user to complete payment
redirect(subscription.payment_page_link)
# Save subscription.id in your user record
Behind the scenes this:
- Creates the customer on PayPlus (
POST /Customers/Add) if not already created - Generates a payment link with
charge_method=3andrecurring_settingsderived from the tier - Saves the subscription locally with
status=INCOMPLETE
The user fills in their card details on the PayPlus hosted page. You never touch card data.
Step 5: Set up the webhook endpoint
from fastapi import FastAPI, Request, HTTPException
from payplus.webhooks import WebhookSignatureError
app = FastAPI()
@app.post("/webhooks/payplus")
async def payplus_webhook(request: Request):
payload = await request.body()
signature = request.headers.get("X-PayPlus-Signature", "")
try:
event = await webhook_handler.handle_async(payload, signature)
await manager.handle_webhook_event(event)
return {"received": True}
except WebhookSignatureError:
raise HTTPException(status_code=400, detail="Invalid signature")
This single endpoint handles every subscription event automatically:
| Webhook event | What happens |
|---|---|
| First payment succeeds | INCOMPLETE -> ACTIVE, recurring_uid stored |
| Recurring charge succeeds | Billing period advanced, status stays ACTIVE |
| Recurring charge fails | Status -> PAST_DUE (-> UNPAID after 4 failures) |
| Recurring canceled | Status -> CANCELED |
| Cancel at period end flagged | After last charge, cancels on PayPlus and sets CANCELED |
Step 6: Check access in your app
sub = await manager.get_subscription(subscription_id)
if sub and sub.is_active:
# User has access
...
Step 7: User upgrades plan
await manager.change_tier(subscription.id, new_tier_id="enterprise")
This updates the recurring payment on PayPlus with the new tier's price and billing cycle. The card token is saved automatically from the first payment webhook.
Step 8: User pauses subscription
await manager.pause_subscription(subscription.id)
# Later, resume it
await manager.resume_subscription(subscription.id)
Step 9: User cancels subscription
# Cancel at end of billing period (user keeps access until then)
await manager.cancel_subscription(
subscription.id,
at_period_end=True,
reason="Customer requested",
)
# Or cancel immediately
await manager.cancel_subscription(subscription.id, at_period_end=False)
Step 10: React to lifecycle events (optional)
Register hooks to trigger your own business logic:
manager.on("subscription.activated", lambda sub: send_welcome_email(sub))
manager.on("subscription.renewed", lambda sub: log_renewal(sub))
manager.on("subscription.payment_failed", lambda sub: send_dunning_email(sub))
manager.on("subscription.canceled", lambda sub: handle_offboarding(sub))
Trials
If a tier has trial_days set, the subscription flow changes:
create_subscription()setsjump_paymentsinrecurring_settings, telling PayPlus to wait N days before the first charge- The subscription starts as
INCOMPLETE(waiting for the user to enter card details on the payment page) - When the user completes the payment page, PayPlus validates the card but doesn't charge yet
- The webhook activates the subscription as
TRIALING(sincetrial_endis in the future) - After the trial period, PayPlus charges automatically and sends a
recurring.chargedwebhook is_activereturnsTruefor bothACTIVEandTRIALINGstatuses
# Tier with a 14-day trial
await manager.create_tier(
tier_id="pro",
name="Pro",
price=Decimal("79"),
trial_days=14, # 14 free days before first charge
)
# After subscription is created and user completes payment page:
# sub.status == "trialing"
# sub.is_active == True
# sub.trial_end == ~14 days from now
How it all fits together
User clicks "Subscribe to Pro"
|
v
create_subscription()
- Creates customer on PayPlus
- Generates payment link with recurring settings
- Subscription status: INCOMPLETE
|
v
User redirected to PayPlus payment page
User enters card details and pays
|
v
PayPlus sends webhook to callback_url
|
v
handle_webhook_event()
- Matches webhook to subscription via page_request_uid
- Saves card token and recurring_uid
- Sets status: ACTIVE (or TRIALING if trial_days > 0)
|
v
Every billing cycle, PayPlus charges automatically
- recurring.charged -> period advanced, still ACTIVE
- recurring.failed -> PAST_DUE (-> UNPAID after 4 failures)
Lifecycle actions (from your app):
- change_tier() -> updates amount on PayPlus
- pause/resume -> updates local status
- cancel(at_period_end=True) -> flags locally, cancels on PayPlus after last charge
- cancel(at_period_end=False) -> cancels on PayPlus immediately, status: CANCELED
Direct API Usage
You can also use the PayPlus API directly without the subscription manager:
Payment Link
result = client.payment_pages.generate_link(
amount=100.00,
currency="ILS",
description="One-time payment",
customer_email="customer@example.com",
success_url="https://yourapp.com/success",
callback_url="https://yourapp.com/webhooks/payplus",
)
print(result["data"]["payment_page_link"])
Payment Link with Recurring
from payplus.api.payment_pages import build_recurring_settings
result = client.payment_pages.generate_link(
amount=79.00,
currency="ILS",
charge_method=3, # Recurring
customer_uid="payplus-customer-uid",
callback_url="https://yourapp.com/webhooks/payplus",
recurring_settings=build_recurring_settings(
billing_cycle="monthly",
trial_days=14,
number_of_charges=0, # Unlimited
),
)
Create Customer
result = client.customers.add(
customer_name="John Doe",
email="john@example.com",
phone="050-1234567",
)
customer_uid = result["data"]["customer_uid"]
Transactions
# Charge a saved card token
result = client.transactions.charge(
token="card_token",
amount=99.00,
currency="ILS",
)
# Refund
client.transactions.refund(
transaction_uid=result["data"]["transaction_uid"],
amount=99.00,
)
Recurring Payments
# Create recurring from token
result = client.recurring.add(
token="card_token",
amount=49.00,
interval="month",
)
# Cancel
client.recurring.cancel(result["data"]["recurring_uid"])
Storage Backends
MongoDB
from motor.motor_asyncio import AsyncIOMotorClient
from payplus.subscriptions.storage import MongoDBStorage
mongo = AsyncIOMotorClient("mongodb://localhost:27017")
storage = MongoDBStorage(mongo.your_database)
await storage.create_indexes() # Run once
SQLAlchemy (PostgreSQL, MySQL, SQLite)
from sqlalchemy.ext.asyncio import create_async_engine
from payplus.subscriptions.storage import SQLAlchemyStorage
engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")
storage = SQLAlchemyStorage(engine)
await storage.create_tables() # Run once
In-Memory (development/testing)
# Used automatically when no storage is provided
manager = SubscriptionManager(client)
API Reference
PayPlus Client
| Module | Methods |
|---|---|
client.customers |
add() |
client.payment_pages |
generate_link(), get_status() |
client.transactions |
charge(), get(), refund(), list() |
client.recurring |
add(), update(), charge(), cancel(), get(), list() |
client.payments |
check_card(), tokenize(), get_token(), delete_token() |
Subscription Manager
| Method | Description |
|---|---|
create_customer() |
Create a new customer |
get_customer() |
Get a customer by ID |
create_tier() |
Create a pricing tier |
get_tier() |
Get a tier by ID |
list_tiers() |
List all tiers |
create_subscription() |
Create subscription and generate payment link |
get_subscription() |
Get a subscription by ID |
change_tier() |
Upgrade/downgrade (updates PayPlus recurring) |
pause_subscription() |
Pause a subscription |
resume_subscription() |
Resume a paused subscription |
cancel_subscription() |
Cancel immediately or at period end |
handle_webhook_event() |
Process webhook and update subscription state |
Configuration
PAYPLUS_API_KEY=your_api_key
PAYPLUS_SECRET_KEY=your_secret_key
PAYPLUS_TERMINAL_UID=your_terminal_uid # Optional
PAYPLUS_SANDBOX=true # For testing
# Sandbox (restapidev.payplus.co.il)
client = PayPlus(api_key="...", secret_key="...", sandbox=True)
# Production (restapi.payplus.co.il)
client = PayPlus(api_key="...", secret_key="...", sandbox=False)
Error Handling
from payplus.exceptions import (
PayPlusError,
PayPlusAPIError,
PayPlusAuthError,
SubscriptionError,
WebhookSignatureError,
)
try:
result = client.transactions.charge(token="...", amount=100)
except PayPlusAuthError:
print("Invalid API credentials")
except PayPlusAPIError as e:
print(f"API error [{e.status_code}]: {e.message}")
except PayPlusError as e:
print(f"General error: {e}")
License
MIT License - see LICENSE for details.
Links
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file payplus_python-0.2.1.tar.gz.
File metadata
- Download URL: payplus_python-0.2.1.tar.gz
- Upload date:
- Size: 36.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e5d66cf63733e5a8188c4903e28a50a610956111cc77899fc1f33e4fd587d488
|
|
| MD5 |
b4136918f5760aa0d380038f0a6c08ab
|
|
| BLAKE2b-256 |
7bf316a074b310a7424d093fc8cb5d25ff02db9fe9c6f38a213e6bf8236f53a5
|
Provenance
The following attestation bundles were made for payplus_python-0.2.1.tar.gz:
Publisher:
publish.yml on Two-Solutions/payplus-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
payplus_python-0.2.1.tar.gz -
Subject digest:
e5d66cf63733e5a8188c4903e28a50a610956111cc77899fc1f33e4fd587d488 - Sigstore transparency entry: 1244268722
- Sigstore integration time:
-
Permalink:
Two-Solutions/payplus-python@29ed9f0b6d6288cc9487c29de248e8a0f697ecc7 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/Two-Solutions
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@29ed9f0b6d6288cc9487c29de248e8a0f697ecc7 -
Trigger Event:
push
-
Statement type:
File details
Details for the file payplus_python-0.2.1-py3-none-any.whl.
File metadata
- Download URL: payplus_python-0.2.1-py3-none-any.whl
- Upload date:
- Size: 41.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c72a99e2187ededec335830a0526ab6d4a60dab3f76bbcb1d50ceb94859c3c6
|
|
| MD5 |
be1a1e5a86f1bdcfc9d69b64f021da80
|
|
| BLAKE2b-256 |
53f148d7c5e47907a8282624833b25740f3a9b5076e47ff7b25a03bb92f83661
|
Provenance
The following attestation bundles were made for payplus_python-0.2.1-py3-none-any.whl:
Publisher:
publish.yml on Two-Solutions/payplus-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
payplus_python-0.2.1-py3-none-any.whl -
Subject digest:
5c72a99e2187ededec335830a0526ab6d4a60dab3f76bbcb1d50ceb94859c3c6 - Sigstore transparency entry: 1244268725
- Sigstore integration time:
-
Permalink:
Two-Solutions/payplus-python@29ed9f0b6d6288cc9487c29de248e8a0f697ecc7 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/Two-Solutions
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@29ed9f0b6d6288cc9487c29de248e8a0f697ecc7 -
Trigger Event:
push
-
Statement type: