Paper.id Payment Gateway Integration¶
Complete guide to Paper.id payment gateway integration in the Reserva platform, covering all payment flows, data synchronization, webhook handling, and database connections.
๐ฏ Overview¶
The Reserva platform integrates with Paper.id as the primary payment gateway for processing all payment transactions in Indonesia. This integration handles three main payment flows:
- Subscription Payments - Merchants paying for platform subscription plans (FREE โ PRO โ ENTERPRISE)
- Appointment Payments - Customers paying for service bookings
- Wallet Top-ups - Customers adding funds to their digital wallet
Key Integration Points:
- Partner account creation during tenant registration
- Sales invoice generation for B2B transactions
- Real-time payment webhooks for status updates
- Merchant balance tracking and withdrawals
- Platform fee calculations
๐๏ธ Architecture Overview¶
graph TB
subgraph "Reserva Platform"
API[FastAPI Backend]
DB[(MongoDB Database)]
PaperClient[Paper.id Client]
WebhookHandler[Webhook Handler]
API --> PaperClient
API --> DB
WebhookHandler --> DB
end
subgraph "Paper.id Gateway"
PaperAPI[Paper.id API]
PaperWebhook[Paper.id Webhooks]
end
subgraph "Customer Journey"
Customer[Customer/Merchant]
PaymentPage[Paper.id Payment Page]
end
PaperClient -->|Create Invoice| PaperAPI
PaperClient -->|Create Partner| PaperAPI
PaperClient -->|Create Withdrawal| PaperAPI
PaperAPI -->|Payment URL| PaymentPage
Customer -->|Complete Payment| PaymentPage
PaperWebhook -->|Payment Status| WebhookHandler
Customer -->|API Request| API
๐ฆ Database Collections & Paper.id Mapping¶
Core Collections¶
1. tenants Collection¶
Stores business information and Paper.id partner linkage.
Schema:
{
_id: ObjectId,
name: String, // Business name
slug: String, // URL-friendly identifier
email: String, // Business contact email
phone: String, // Business phone (E.164 format)
// Paper.id Integration
client_partner_id: String, // Paper.id partner ID (e.g., "partner_abc123xyz")
// Subscription
subscription: {
plan: String, // "free", "pro", "enterprise"
status: String, // "active", "past_due", "canceled"
current_period_start: Date,
current_period_end: Date
},
// Timestamps
created_at: Date,
updated_at: Date,
is_active: Boolean
}
Paper.id Connection:
client_partner_idlinks to Paper.id partner account- Created automatically during tenant registration via
/api/v2/partners - Used for all invoices and payments for this tenant
2. invoices Collection¶
Stores invoice records synchronized with Paper.id sales invoices.
Schema:
{
_id: ObjectId,
tenant_id: ObjectId, // FK to tenants
invoice_number: String, // "INV-202501-001"
invoice_type: String, // "subscription", "appointment"
// Paper.id Integration Fields
paper_invoice_id: String, // Paper.id invoice ID (e.g., "PI-20250115-ABC123")
paper_pdf_url: String, // URL to invoice PDF hosted on Paper.id
paper_payment_url: String, // Short payment link (e.g., "https://payper.id/short123")
// Billing Details
invoice_date: Date,
due_date: Date,
subtotal: Decimal,
tax_amount: Decimal,
discount_amount: Decimal,
total_amount: Decimal,
paid_amount: Decimal,
// Status Tracking
status: String, // "draft", "sent", "paid", "overdue", "cancelled"
// Line Items
line_items: [
{
description: String,
quantity: Number,
unit_price: Decimal,
amount: Decimal,
tax_rate: Number,
tax_amount: Decimal
}
],
// Metadata for routing
metadata: {
renewal: Boolean, // true = renewal, false/absent = upgrade
subscription_id: ObjectId, // For subscription invoices
appointment_id: ObjectId, // For appointment invoices
previous_plan: String, // For upgrades
new_plan: String // For upgrades
},
// Timestamps
created_at: Date,
updated_at: Date,
paid_at: Date
}
Paper.id Connection:
- Created via
/api/v1/store-invoice(Sales Invoice API) paper_invoice_idis the primary key for Paper.id invoicepaper_payment_urlredirects customer to payment page- Webhook updates use
paper_invoice_idto find local invoice
3. payments Collection¶
Tracks payment transactions processed through Paper.id.
Schema:
{
_id: ObjectId,
tenant_id: ObjectId, // FK to tenants
// Payment Type Routing
payment_type: String, // "subscription", "appointment", "wallet_topup"
subscription_id: ObjectId, // For subscription payments
appointment_id: ObjectId, // For appointment payments
customer_id: ObjectId, // For customer payments
// Paper.id Integration
paper_transaction_id: String, // Paper.id payment transaction ID
reference_id: String, // Our reference ID (sent to Paper.id)
// Amounts
amount: Decimal, // Base amount
platform_fee: Decimal, // Fee charged to merchant (5% default)
platform_fee_rate: Decimal, // Fee percentage (0.05 = 5%)
merchant_amount: Decimal, // Amount credited to merchant (amount - fee)
total_amount: Decimal, // Total paid by customer
currency: String, // "IDR"
// Payment Details
payment_method: String, // "bank_transfer", "e_wallet", "qris"
payment_provider: String, // "paper_id"
status: String, // "pending", "processing", "completed", "failed"
// Gateway Data
gateway_status: String, // Raw status from Paper.id (e.g., "PAID", "PENDING")
gateway_data: Object, // Full webhook payload from Paper.id
// Timestamps
created_at: Date,
updated_at: Date,
completed_at: Date
}
Paper.id Connection:
reference_idsent to Paper.id during invoice creationpaper_transaction_idreceived from Paper.id webhookgateway_datastores complete webhook payload for debugging- Status synchronized via webhook callbacks
4. subscriptions Collection¶
Manages subscription plans and billing cycles.
Schema:
{
_id: ObjectId,
tenant_id: ObjectId, // FK to tenants
// Plan Details
plan_type: String, // "free", "pro", "enterprise"
billing_cycle: String, // "monthly", "quarterly", "yearly"
// Period Tracking
current_period_start: Date,
current_period_end: Date,
next_billing_date: Date,
// Status
status: String, // "active", "past_due", "canceled", "expired"
auto_renew: Boolean,
// Scheduled Changes (for downgrades)
scheduled_changes: {
target_plan: String,
effective_date: Date,
reason: String
},
// Timestamps
created_at: Date,
updated_at: Date
}
Paper.id Connection:
- Invoices generated for upgrades/renewals
- Invoice metadata contains
subscription_idfor linking - Webhook handlers update subscription status after payment
5. merchant_balances Collection¶
Tracks merchant earnings and available balance for withdrawals.
Schema:
{
_id: ObjectId,
tenant_id: ObjectId, // FK to tenants (unique index)
// Balance Tracking
available_balance: Decimal, // Available for withdrawal
pending_balance: Decimal, // Pending settlement (not yet available)
total_earned: Decimal, // Lifetime earnings
total_withdrawn: Decimal, // Lifetime withdrawals
// Paper.id Integration
paper_partner_id: String, // Paper.id partner ID
last_synced_at: Date, // Last sync with Paper.id balance API
// Timestamps
created_at: Date,
updated_at: Date
}
Paper.id Connection:
- Updated when appointment payments complete
- Synchronized with Paper.id balance via
/api/v1/balance - Used for withdrawal validations
6. withdrawals Collection¶
Tracks merchant withdrawal requests.
Schema:
{
_id: ObjectId,
tenant_id: ObjectId, // FK to tenants
// Withdrawal Details
amount: Decimal,
currency: String, // "IDR"
bank_account_id: String, // Paper.id bank account ID
// Paper.id Integration
paper_withdrawal_id: String, // Paper.id withdrawal transaction ID
otp_verified: Boolean,
// Status Tracking
status: String, // "pending", "processing", "completed", "failed", "rejected"
// Timestamps
requested_at: Date,
processed_at: Date,
completed_at: Date
}
Paper.id Connection:
- Created via
/api/v2/withdrawalswith OTP verification - Status updated via Paper.id webhook
- Merchant balance reduced when approved
๐ Data Flow Diagrams¶
Flow 1: Tenant Registration โ Partner Creation¶
sequenceDiagram
participant User
participant API as FastAPI
participant DB as MongoDB
participant Paper as Paper.id API
User->>API: POST /api/v1/public/register
Note over User,API: business_name, email, phone, admin_email, password
API->>DB: Check for duplicates
DB-->>API: No duplicates found
API->>DB: Create tenant record
Note over DB: tenant_id generated
API->>Paper: POST /api/v2/partners
Note over API,Paper: Create partner account<br/>name, email, phone, type=CLIENT
Paper-->>API: Partner created
Note over Paper: client_partner_id returned
API->>DB: Update tenant.client_partner_id
API->>DB: Create admin user
API->>DB: Create subscription (FREE plan)
API-->>User: Registration successful
Note over User: tenant_id, slug, client_partner_id
Key Points:
- Partner creation happens automatically during tenant registration
client_partner_idis stored intenants.client_partner_id- Partner ID format:
myreserva-{tenant_id} - If Paper.id creation fails, tenant is still created (manual partner setup needed)
Flow 2: Subscription Upgrade โ Invoice โ Payment โ Webhook¶
sequenceDiagram
participant Tenant
participant API as FastAPI
participant DB as MongoDB
participant Paper as Paper.id API
participant Customer
participant Webhook as Webhook Handler
Tenant->>API: POST /api/v1/subscriptions/upgrade
Note over Tenant: target_plan=pro, billing_period=monthly
API->>DB: Get current subscription
API->>API: Calculate prorated amount
Note over API: (new_price - old_price) * (days_remaining / days_in_period)
API->>DB: Create invoice record
Note over DB: invoice_type=subscription<br/>status=draft<br/>metadata.renewal=false
API->>Paper: POST /api/v1/store-invoice
Note over API,Paper: Customer info, items, amounts<br/>callback_url, metadata
Paper-->>API: Invoice created
Note over Paper: paper_invoice_id<br/>paper_payment_url<br/>paper_pdf_url
API->>DB: Update invoice with Paper.id details
Note over DB: paper_invoice_id saved<br/>status=sent
API-->>Tenant: Invoice created
Note over Tenant: payment_url returned
Tenant->>Customer: Share payment link
Customer->>Paper: Open paper_payment_url
Customer->>Paper: Complete payment
Paper->>Webhook: POST /api/v1/webhooks/paper-invoice
Note over Paper,Webhook: X-Paper-Signature header<br/>invoice paid payload
Webhook->>Webhook: Verify HMAC signature
Webhook->>DB: Find invoice by paper_invoice_id
Webhook->>DB: Update invoice.status = paid
Webhook->>DB: Check metadata.renewal flag
Note over Webhook: renewal=false โ upgrade handler
Webhook->>DB: Update subscription.plan_type = pro
Note over DB: current_period_end unchanged
Webhook-->>Paper: 200 OK
Webhook->>Tenant: Send upgrade confirmation email
Key Database Updates:
- Invoice created:
invoicescollection withstatus=draft - Paper.id invoice:
paper_invoice_id,paper_payment_url,paper_pdf_urladded - Invoice sent:
statuschanged tosent - Webhook received:
statuschanged topaid,paid_attimestamp set - Subscription updated:
subscriptions.plan_typechanged,current_period_endunchanged
Flow 3: Subscription Renewal โ Extended Period¶
sequenceDiagram
participant Tenant
participant API
participant DB as MongoDB
participant Paper as Paper.id
participant Webhook
Tenant->>API: POST /api/v1/subscriptions/renew
Note over Tenant: subscription_id
API->>DB: Get subscription details
API->>API: Calculate renewal amount (full period)
API->>DB: Create invoice
Note over DB: metadata.renewal = TRUE<br/>metadata.billing_cycle = monthly
API->>Paper: POST /api/v1/store-invoice
Note over API,Paper: Full period amount<br/>renewal flag in metadata
Paper-->>API: Invoice created
API->>DB: Update invoice with Paper.id URLs
API-->>Tenant: Payment URL returned
Tenant->>Paper: Complete payment
Paper->>Webhook: POST /api/v1/webhooks/paper-invoice
Webhook->>DB: Find invoice by paper_invoice_id
Webhook->>DB: Check metadata.renewal = true
Note over Webhook: Routes to RENEWAL handler
Webhook->>DB: Extend subscription period
Note over DB: current_period_end += 30 days<br/>current_period_start = old end date<br/>plan_type UNCHANGED
Webhook-->>Paper: 200 OK
Key Differences from Upgrade:
| Aspect | Upgrade | Renewal |
|---|---|---|
| Invoice metadata | renewal: false or absent |
renewal: true |
| Amount | Prorated (remaining days) | Full period |
| Subscription change | plan_type changes |
plan_type unchanged |
| Period change | current_period_end unchanged |
current_period_end extended by billing cycle |
| Webhook handler | handle_invoice_payment_for_subscription() |
handle_invoice_payment_for_renewal() |
Flow 4: Appointment Payment โ Merchant Balance¶
sequenceDiagram
participant Customer
participant API
participant DB as MongoDB
participant Paper as Paper.id
participant Webhook
Customer->>API: POST /api/v1/customer/booking/appointments
Note over Customer: service_id, outlet_id, staff_id, date, time
API->>DB: Create appointment
Note over DB: status=pending<br/>payment_status=unpaid
API->>DB: Create invoice
Note over DB: invoice_type=appointment<br/>metadata.appointment_id
API->>Paper: POST /api/v1/store-invoice
Paper-->>API: Invoice created
API->>DB: Update invoice with Paper.id details
API-->>Customer: Payment URL
Customer->>Paper: Complete payment
Paper->>Webhook: POST /api/v1/webhooks/paper-invoice
Webhook->>DB: Find invoice by paper_invoice_id
Webhook->>DB: Update invoice.status = paid
Webhook->>DB: Get appointment_id from metadata
Webhook->>DB: Update appointment.status = confirmed
Webhook->>DB: Update appointment.payment_status = paid
Webhook->>Webhook: Calculate platform fee (5%)
Note over Webhook: platform_fee = amount * 0.05<br/>merchant_amount = amount - platform_fee
Webhook->>DB: Update merchant_balances
Note over DB: available_balance += merchant_amount<br/>total_earned += merchant_amount
Webhook->>DB: Create payment record
Note over DB: payment_type=appointment<br/>platform_fee saved<br/>merchant_amount saved
Webhook-->>Paper: 200 OK
Webhook->>Customer: Send booking confirmation email
Database Updates:
- Appointment created:
status=pending,payment_status=unpaid - Invoice created: Linked via
metadata.appointment_id - Payment completed: Appointment becomes
confirmed,paid - Merchant balance:
available_balanceincreased bymerchant_amount(amount - 5% platform fee) - Payment record: Complete audit trail with fee breakdown
๐ Webhook Architecture¶
Single Endpoint, Multiple Handlers¶
Paper.id allows only one webhook URL per account. The platform uses intelligent routing to handle different invoice types:
Webhook URL: POST /api/v1/webhooks/paper-invoice
Routing Logic:
async def handle_paper_invoice_webhook(payload: dict):
invoice_data = payload.get('data', {})
invoice_type = invoice_data.get('invoice_type')
metadata = invoice_data.get('metadata', {})
# Route based on invoice_type and metadata
if invoice_type == "SUBSCRIPTION":
if metadata.get('renewal') == True:
# Renewal: extend period, keep plan
await handle_invoice_payment_for_renewal(invoice_data)
else:
# Upgrade: change plan, keep period
await handle_invoice_payment_for_subscription(invoice_data)
elif invoice_type == "APPOINTMENT":
# Appointment booking payment
await handle_invoice_payment_for_appointment(invoice_data)
Webhook Payload Structure¶
Paper.id sends:
{
"event": "invoice.paid",
"data": {
"invoice_id": "PI-20250115-ABC123",
"invoice": {
"id": "507f1f77bcf86cd799439011",
"number": "INV-202501-001",
"status": "paid",
"amount": "249950",
"paid_amount": "249950"
},
"invoice_type": "SUBSCRIPTION",
"metadata": {
"tenant_id": "12345abcdef67890abcdef12",
"subscription_id": "67890abcdef1234567890123",
"renewal": false,
"previous_plan": "free",
"new_plan": "pro"
},
"paid_at": "2025-01-15T14:30:00Z"
}
}
Security: HMAC Signature Verification¶
Header: X-Paper-Signature: sha256_abc123def456...
Verification Process:
def verify_signature(payload: str, signature: str) -> bool:
# Remove prefix if present
if signature.startswith('sha256_'):
signature = signature[7:]
# Calculate expected signature
expected = hmac.new(
client_secret.encode(),
payload.encode(),
hashlib.sha256
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(expected, signature)
Security Measures:
- โ HMAC-SHA256 signature verification
- โ Idempotency checks (track processed invoice IDs)
- โ Amount verification (payment matches invoice)
- โ Tenant isolation (invoice belongs to correct tenant)
- โ IP whitelisting (production: only accept from Paper.id IPs)
๐ณ Paper.id API Endpoints Used¶
Partner Management¶
Create Partner¶
POST /api/v2/partners
Headers:
client_id: {PAPER_ID_CLIENT_ID}
client_secret: {PAPER_ID_CLIENT_SECRET}
Body:
{
"name": "Bella Vista Spa",
"number": "myreserva-507f1f77bcf86cd799439011",
"phone": "628123456789",
"type": "CLIENT",
"email": "contact@bellavista.com",
"business_type": "pt",
"address": {
"address_line_1": "Jl. Example No. 123",
"district": "Jakarta Selatan",
"province": "DKI Jakarta",
"zip_code": "12345",
"country": "IDN"
}
}
Response:
{
"data": {
"id": "partner_abc123xyz",
"number": "myreserva-507f1f77bcf86cd799439011",
"name": "Bella Vista Spa",
...
}
}
Database Update:
db.tenants.updateOne(
{ _id: ObjectId("507f1f77bcf86cd799439011") },
{ $set: { client_partner_id: "partner_abc123xyz" } }
)
Sales Invoice API¶
Create Invoice¶
POST /api/v1/store-invoice
Headers:
client_id: {PAPER_ID_CLIENT_ID}
client_secret: {PAPER_ID_CLIENT_SECRET}
Body:
{
"invoice_date": "15-01-2025",
"due_date": "22-01-2025",
"customer": {
"id": "myreserva-507f1f77bcf86cd799439011",
"name": "Bella Vista Spa",
"email": "contact@bellavista.com",
"phone": "628123456789"
},
"items": [
{
"item_name": "PRO Plan - Monthly Subscription",
"unit": "month",
"unit_count": 1,
"unit_price": 249950,
"amount": 249950
}
],
"callback_url": "https://api.myreserva.com/api/v1/webhooks/paper-invoice",
"send": {
"email": true,
"whatsapp": false,
"sms": false
},
"metadata": {
"tenant_id": "507f1f77bcf86cd799439011",
"subscription_id": "67890abcdef1234567890123",
"renewal": false,
"previous_plan": "free",
"new_plan": "pro"
}
}
Response:
{
"data": {
"invoice_id": "PI-20250115-ABC123",
"invoice_url": "https://stg-v2.paper.id/invoice/abc123",
"pdf_url": "https://stg-v2.paper.id/pdf/abc123.pdf",
"short_url": "https://payper.id/short123",
"status": "unpaid"
}
}
Database Update:
db.invoices.updateOne(
{ _id: ObjectId("...") },
{
$set: {
paper_invoice_id: "PI-20250115-ABC123",
paper_payment_url: "https://payper.id/short123",
paper_pdf_url: "https://stg-v2.paper.id/pdf/abc123.pdf",
status: "sent"
}
}
)
Withdrawal API¶
Request OTP¶
GET /api/v2/verification-request?otp_action=withdrawal&delivery_method=email&email=admin@example.com
Headers:
client_id: {PAPER_ID_CLIENT_ID}
client_secret: {PAPER_ID_CLIENT_SECRET}
Response:
{
"data": {
"otp_sent": true,
"delivery_method": "email",
"expires_in": 300
}
}
Create Withdrawal¶
POST /api/v2/withdrawals
Headers:
client_id: {PAPER_ID_CLIENT_ID}
client_secret: {PAPER_ID_CLIENT_SECRET}
Body:
{
"amount": 1000000,
"bank_account_id": "ba_abc123",
"otp_code": "123456",
"description": "Merchant withdrawal"
}
Response:
{
"data": {
"withdrawal_id": "WD-20250115-XYZ789",
"amount": 1000000,
"status": "processing",
"created_at": "2025-01-15T14:30:00Z"
}
}
Database Update:
// Create withdrawal record
db.withdrawals.insertOne({
tenant_id: ObjectId("..."),
amount: 1000000,
paper_withdrawal_id: "WD-20250115-XYZ789",
status: "processing",
requested_at: new Date()
})
// Update merchant balance
db.merchant_balances.updateOne(
{ tenant_id: ObjectId("...") },
{
$inc: {
available_balance: -1000000,
total_withdrawn: 1000000
}
}
)
๐ Complete Integration Checklist¶
โ Tenant Onboarding¶
- [ ] User submits registration form
- [ ] System creates tenant record in
tenantscollection - [ ] System calls Paper.id
/api/v2/partnersto create partner - [ ] System stores
client_partner_idin tenant record - [ ] System creates FREE subscription in
subscriptionscollection - [ ] System creates admin user in
userscollection - [ ] System returns registration confirmation with
tenant_id,slug,client_partner_id
โ Subscription Upgrade¶
- [ ] Tenant requests upgrade via
/api/v1/subscriptions/upgrade - [ ] System calculates prorated amount
- [ ] System creates invoice in
invoicescollection withmetadata.renewal=false - [ ] System calls Paper.id
/api/v1/store-invoice - [ ] System updates invoice with
paper_invoice_id,paper_payment_url,paper_pdf_url - [ ] System returns payment URL to tenant
- [ ] Customer completes payment on Paper.id
- [ ] Paper.id sends webhook to
/api/v1/webhooks/paper-invoice - [ ] System verifies HMAC signature
- [ ] System routes to upgrade handler (no renewal flag)
- [ ] System updates
subscription.plan_typeto new plan - [ ] System keeps
current_period_endunchanged - [ ] System marks invoice as
paid - [ ] System sends upgrade confirmation email
โ Subscription Renewal¶
- [ ] Tenant requests renewal via
/api/v1/subscriptions/renew - [ ] System creates invoice with
metadata.renewal=true - [ ] System calls Paper.id
/api/v1/store-invoicewith full period amount - [ ] System returns payment URL
- [ ] Customer completes payment
- [ ] Paper.id sends webhook
- [ ] System routes to renewal handler (renewal flag present)
- [ ] System extends
current_period_endby billing cycle duration - [ ] System keeps
plan_typeunchanged - [ ] System sends renewal confirmation email
โ Appointment Payment¶
- [ ] Customer creates appointment via
/api/v1/customer/booking/appointments - [ ] System creates appointment with
status=pending,payment_status=unpaid - [ ] System creates invoice with
invoice_type=appointment,metadata.appointment_id - [ ] System calls Paper.id
/api/v1/store-invoice - [ ] System returns payment URL
- [ ] Customer completes payment
- [ ] Paper.id sends webhook
- [ ] System updates invoice to
paid - [ ] System updates appointment to
confirmed,paid - [ ] System calculates platform fee (5%)
- [ ] System updates
merchant_balances.available_balancewith merchant amount - [ ] System creates payment record with fee breakdown
- [ ] System sends booking confirmation email
โ Merchant Withdrawal¶
- [ ] Merchant requests OTP via
/api/v1/withdrawals/request-otp - [ ] System calls Paper.id
/api/v2/verification-request - [ ] Merchant receives OTP via email
- [ ] Merchant submits withdrawal with OTP via
/api/v1/withdrawals - [ ] System validates balance and OTP
- [ ] System calls Paper.id
/api/v2/withdrawals - [ ] System creates withdrawal record in
withdrawalscollection - [ ] System reduces
merchant_balances.available_balance - [ ] Paper.id processes withdrawal
- [ ] Paper.id sends webhook with status update
- [ ] System updates withdrawal status to
completed
๐งช Testing Guide¶
Prerequisites¶
-
ngrok - Expose local server for webhook testing
-
Paper.id Dashboard - Configure webhook URL
-
Go to Settings โ Webhooks
- Set URL:
https://your-ngrok-url.ngrok.io/api/v1/webhooks/paper-invoice -
Save
-
Test Credentials - Use Paper.id staging environment
Test Scenario 1: Tenant Registration¶
# 1. Register new tenant
curl -X POST http://localhost:8000/api/v1/public/register \
-H "Content-Type: application/json" \
-d '{
"business_name": "Test Spa",
"business_email": "test@spa.com",
"business_phone": "+628123456789",
"admin_email": "admin@test.com",
"admin_password": "SecurePass123!",
"terms_accepted": true,
"privacy_accepted": true
}'
# 2. Verify in MongoDB
mongo beauty_saas_db
db.tenants.findOne({ name: "Test Spa" })
# Check: client_partner_id is populated
# 3. Verify in Paper.id Dashboard
# Go to Partners โ Search for "myreserva-{tenant_id}"
Test Scenario 2: Subscription Upgrade¶
# 1. Get current subscription
curl -X GET http://localhost:8000/api/v1/subscriptions/current \
-H "Authorization: Bearer YOUR_JWT_TOKEN"
# 2. Request upgrade
curl -X POST http://localhost:8000/api/v1/subscriptions/upgrade \
-H "Authorization: Bearer YOUR_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"target_plan": "pro",
"billing_period": "monthly",
"prorate_charges": true
}'
# Response contains paper_payment_url
# 3. Check invoice in MongoDB
db.invoices.findOne({ paper_invoice_id: "PI-..." })
# Verify: metadata.renewal is false or absent
# 4. Complete payment (open paper_payment_url in browser)
# 5. Monitor webhook
tail -f logs/app.log | grep "Paper.id webhook"
# 6. Verify subscription updated
db.subscriptions.findOne({ tenant_id: ObjectId("...") })
# Check: plan_type changed, current_period_end unchanged
Test Scenario 3: Appointment Payment¶
# 1. Create appointment
curl -X POST http://localhost:8000/api/v1/customer/booking/appointments \
-H "Authorization: Bearer CUSTOMER_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"outlet_id": "...",
"service_id": "...",
"staff_id": "...",
"appointment_date": "2025-01-20",
"appointment_time": "14:00"
}'
# 2. Check appointment status
db.appointments.findOne({ _id: ObjectId("...") })
# Verify: status=pending, payment_status=unpaid
# 3. Complete payment via returned URL
# 4. Verify after webhook
db.appointments.findOne({ _id: ObjectId("...") })
# Check: status=confirmed, payment_status=paid
db.merchant_balances.findOne({ tenant_id: ObjectId("...") })
# Check: available_balance increased
๐ Environment Variables¶
# Paper.id Configuration
PAPER_ID_BASE_URL=https://open-api.stag-v2.paper.id # Staging
# PAPER_ID_BASE_URL=https://open-api.paper.id # Production
PAPER_ID_CLIENT_ID=your_client_id_here
PAPER_ID_CLIENT_SECRET=your_client_secret_here
# Backend URL (for webhook callbacks)
BACKEND_URL=https://api.myreserva.com
# Optional: Next.js webhook proxy
NEXTJS_WEBHOOK_URL=https://app.myreserva.com/api/webhooks
๐จ Troubleshooting¶
Issue: Partner Creation Fails¶
Symptoms: Tenant created but client_partner_id is empty
Checks:
Solutions:
- Verify Paper.id credentials in
.env - Check phone number format (E.164 without +)
- Manually create partner via Paper.id dashboard
- Update tenant record:
Issue: Webhook Not Received¶
Symptoms: Payment completed but status not updated
Checks:
- ngrok running:
ngrok http 8000 - Webhook URL configured in Paper.id dashboard
- Check ngrok web interface:
http://localhost:4040 - Check server logs:
tail -f logs/app.log
Solutions:
- Resend webhook from Paper.id dashboard
- Manually update invoice status:
Issue: Wrong Handler Invoked¶
Symptoms: Renewal processed as upgrade (or vice versa)
Checks:
Solutions:
- Verify invoice metadata during creation
- If incorrect, revert subscription changes manually
- Correct metadata and resend webhook
๐ Monitoring & Metrics¶
Key Metrics to Track¶
-
Partner Creation Success Rate
-
Webhook Processing Success Rate
-
Merchant Balance Accuracy
db.merchant_balances.find({}).forEach(function(balance) { var earned = db.payments.aggregate([ { $match: { tenant_id: balance.tenant_id, status: "completed" } }, { $group: { _id: null, total: { $sum: "$merchant_amount" } } } ]).toArray()[0].total; var withdrawn = db.withdrawals.aggregate([ { $match: { tenant_id: balance.tenant_id, status: "completed" } }, { $group: { _id: null, total: { $sum: "$amount" } } } ]).toArray()[0].total; print("Tenant: " + balance.tenant_id); print("Expected: " + (earned - withdrawn)); print("Actual: " + balance.available_balance); });
๐ Additional Resources¶
- Paper.id Documentation: Paper.id Open API
- API Reference: Swagger UI
- Subscription Management: Subscription Management
- Tenant Management: Tenant Management