Skip to content

Subscription Management

Complete guide to managing subscriptions, upgrades, renewals, and billing in the Reserva platform.


Overview

The subscription system provides flexible tiered plans with support for:

  • Plan Upgrades - Change to a higher/lower tier with prorated billing
  • Subscription Renewals - Extend subscription period with same plan
  • Usage Tracking - Monitor consumption against plan limits
  • Payment History - View all subscription-related payments
  • Scheduled Downgrades - Plan downgrades applied at next billing cycle
  • Cancellations - End subscription with grace period

Key Concepts:

  • Upgrade = Plan changes (FREE→PRO→ENTERPRISE), period stays the same
  • Renewal = Period extends, plan stays the same
  • Prorated Billing = Upgrades charge difference for remaining period
  • Full Period Billing = Renewals charge full amount for new period

Available Plans

Get the list of available subscription plans with pricing and limits.

Endpoint

GET /api/v1/subscriptions/plans

Authentication: Required (JWT token)

Response

{
  "plans": [
    {
      "plan_type": "FREE",
      "display_name": "Free Plan",
      "description": "Perfect for getting started",
      "price": {
        "monthly": 0,
        "quarterly": 0,
        "yearly": 0,
        "currency": "IDR"
      },
      "limits": {
        "max_outlets": 1,
        "max_staff_per_outlet": 5,
        "max_appointments_per_month": 100,
        "max_services": 10
      },
      "features": [
        "Basic booking management",
        "Email notifications",
        "Customer portal"
      ]
    },
    {
      "plan_type": "PRO",
      "display_name": "Pro Plan",
      "description": "For established businesses",
      "price": {
        "monthly": 599000,
        "quarterly": 1617300,
        "yearly": 6468000,
        "currency": "IDR"
      },
      "limits": {
        "max_outlets": 10,
        "max_staff_per_outlet": 50,
        "max_appointments_per_month": 2000,
        "max_services": 50
      },
      "features": [
        "Everything in Free",
        "API access",
        "Waitlist management",
        "Loyalty programs",
        "Priority support"
      ]
    },
    {
      "plan_type": "ENTERPRISE",
      "display_name": "Enterprise Plan",
      "description": "For large organizations",
      "price": {
        "monthly": 1499000,
        "quarterly": 4047300,
        "yearly": 16188000,
        "currency": "IDR"
      },
      "limits": {
        "max_outlets": -1,
        "max_staff_per_outlet": -1,
        "max_appointments_per_month": -1,
        "max_services": -1
      },
      "features": [
        "Everything in Pro",
        "Unlimited everything",
        "Dedicated account manager",
        "Custom integrations",
        "SLA guarantee"
      ]
    }
  ]
}

Note: -1 means unlimited for that limit.


Get Current Subscription

Retrieve detailed information about the tenant's current subscription.

Endpoint

GET /api/v1/subscriptions/current

Authentication: Required (JWT token)

Response

{
  "subscription_id": "67890abcdef1234567890123",
  "tenant_id": "12345abcdef67890abcdef12",
  "plan_type": "PRO",
  "billing_cycle": "monthly",
  "status": "active",
  "current_period_start": "2025-09-07T00:00:00Z",
  "current_period_end": "2025-10-07T00:00:00Z",
  "next_billing_date": "2025-10-07T00:00:00Z",
  "is_trial": false,
  "trial_ends_at": null,
  "auto_renew": true,
  "plan_details": {
    "display_name": "Pro Plan",
    "price": 599000,
    "currency": "IDR",
    "limits": {
      "max_outlets": 10,
      "max_staff_per_outlet": 50,
      "max_appointments_per_month": 2000,
      "max_services": -1
    }
  },
  "usage": {
    "outlets": 3,
    "staff": 12,
    "appointments_this_month": 234
  },
  "scheduled_changes": null
}

Subscription Statuses:

  • active - Subscription is active and paid
  • past_due - Payment failed, grace period active
  • canceled - Subscription canceled, will expire at period end
  • expired - Subscription period ended without renewal

Subscription Upgrade

Change to a different plan tier with prorated billing for the remaining period.

Endpoint

POST /api/v1/subscriptions/upgrade

Authentication: Required (JWT token)

Request Body

{
  "target_plan": "pro",
  "billing_period": "monthly",
  "prorate_charges": true
}

Parameters:

  • target_plan (required) - Target plan: free, pro, enterprise
  • billing_period (optional) - monthly or yearly (defaults to current period)
  • prorate_charges (optional) - Whether to prorate charges (default: true)

Response

{
  "status": "payment_pending",
  "message": "Invoice created successfully. Please complete payment to activate your subscription upgrade.",
  "subscription": {
    "id": "507f1f77bcf86cd799439011",
    "plan": "free",
    "status": "active",
    "current_period_end": "2025-02-15"
  },
  "invoice": {
    "id": "507f1f77bcf86cd799439012",
    "invoice_number": "INV-2025-001",
    "amount": "249950",
    "currency": "IDR",
    "due_date": "2025-01-18",
    "status": "pending",
    "paper_invoice_id": "PI-123456",
    "paper_invoice_url": "https://stg-v2.paper.id/abc123",
    "paper_pdf_url": "https://stg-v2.paper.id/pdf/xyz789",
    "paper_payment_url": "https://payper.id/short123"
  },
  "upgrade_details": {
    "from_plan": "free",
    "to_plan": "pro",
    "prorated_amount": "249950",
    "days_remaining": 15,
    "total_days": 30,
    "billing_period": "monthly",
    "prorated": true
  },
  "next_steps": [
    "1. Open the payment URL to complete payment",
    "2. Choose your preferred payment method",
    "3. Your subscription will be upgraded automatically upon payment confirmation",
    "4. You will receive an email confirmation once activated"
  ]
}

Upgrade Flow (12 Steps)

The upgrade process follows this sequence:

  1. Validation - Check new plan is different from current
  2. Proration Calculation - Calculate prorated charge for remaining period
  3. Invoice Generation - Create subscription upgrade invoice
  4. Paper.id Integration - Generate Paper.id invoice with metadata
  5. Invoice Creation - Store invoice in database
  6. Payment URL - Return payment link to client
  7. Customer Payment - Customer completes payment on Paper.id
  8. Webhook Received - Paper.id sends payment confirmation
  9. Invoice Routing - Webhook router identifies as upgrade (no renewal flag)
  10. Payment Verification - Validate payment amount and status
  11. Subscription Update - Update subscription to new plan
  12. Notification - Send upgrade confirmation email

Webhook Routing for Upgrades:

# Webhook receives invoice payment event
if invoice.invoice_type == "SUBSCRIPTION":
    if invoice.metadata.get('renewal') == True:
        # Route to renewal handler
        handle_invoice_payment_for_renewal()
    else:
        # Route to upgrade handler (NO renewal flag)
        handle_invoice_payment_for_subscription()

Important Notes:

  • Upgrades are NOT applied immediately upon API call
  • Upgrade activates after payment is completed
  • Downgrade to lower plan schedules change for next billing cycle
  • Prorated amount is calculated: (new_price - old_price) × (days_remaining / days_in_period)
  • Invoice expires in 7 days if unpaid

Subscription Renewal

Extend subscription period with the same plan (no plan change).

Endpoint

POST /api/v1/subscriptions/renew

Authentication: Required (JWT token)

Request Body

{
  "subscription_id": "507f1f77bcf86cd799439011"
}

Parameters:

  • subscription_id (required) - Subscription ID to renew

Response

{
  "status": "payment_pending",
  "message": "Renewal invoice created successfully. Please complete payment to continue your subscription.",
  "subscription": {
    "id": "507f1f77bcf86cd799439011",
    "plan": "pro",
    "billing_period": "monthly",
    "current_period_end": "2025-02-15"
  },
  "invoice": {
    "id": "507f1f77bcf86cd799439012",
    "invoice_number": "INV-2025-002",
    "amount": "499900",
    "currency": "IDR",
    "due_date": "2025-02-22",
    "paper_invoice_url": "https://stg-v2.paper.id/abc123",
    "paper_pdf_url": "https://stg-v2.paper.id/pdf/xyz789",
    "paper_payment_url": "https://payper.id/short456"
  },
  "renewal_details": {
    "renewing_plan": "pro",
    "billing_period": "monthly",
    "renewal_amount": "499900",
    "next_period_start": "2025-02-15",
    "next_period_end": "2025-03-15"
  },
  "next_steps": [
    "1. Open the payment URL to complete payment",
    "2. Your subscription will be extended automatically upon payment confirmation",
    "3. You will receive an email confirmation"
  ]
}

Renewal Flow

  1. Validation - Verify subscription is active and can be renewed
  2. Invoice Generation - Create renewal invoice with renewal=true metadata
  3. Paper.id Integration - Generate Paper.id invoice with renewal flag
  4. Full Period Charge - Charge full amount for selected billing cycle (no proration)
  5. Payment URL - Return payment link to client
  6. Customer Payment - Customer completes payment
  7. Webhook Received - Paper.id sends payment confirmation
  8. Invoice Routing - Webhook router identifies renewal flag in metadata
  9. Subscription Extension - Extend current_period_end by billing cycle duration
  10. Notification - Send renewal confirmation email

Webhook Routing for Renewals:

# Webhook receives invoice payment event
if invoice.invoice_type == "SUBSCRIPTION":
    if invoice.metadata.get('renewal') == True:
        # Route to RENEWAL handler (renewal flag present)
        handle_invoice_payment_for_renewal()
    else:
        # Route to upgrade handler
        handle_invoice_payment_for_subscription()

Key Differences from Upgrade:

Aspect Renewal Upgrade
Plan Stays the same Changes to new plan
Period Extends by billing cycle Stays the same
Billing Full period charge Prorated for remaining days
Metadata renewal: true No renewal flag
Webhook Handler handle_invoice_payment_for_renewal() handle_invoice_payment_for_subscription()
Activation Period extended on payment Plan changed on payment

Subscription Downgrade

Schedule a downgrade to a lower-tier plan, effective at next billing cycle.

Endpoint

POST /api/v1/subscriptions/downgrade

Authentication: Required (JWT token)

Request Body

{
  "target_plan": "free",
  "reason": "Reducing business size"
}

Parameters:

  • target_plan (required) - Target plan: free, pro, enterprise
  • reason (optional) - Reason for downgrade

Response

Returns updated subscription with scheduled downgrade:

{
  "id": "507f1f77bcf86cd799439011",
  "tenant_id": "507f1f77bcf86cd799439010",
  "plan": "pro",
  "status": "active",
  "billing_period": "monthly",
  "current_period_start": "2025-01-15",
  "current_period_end": "2025-02-15",
  "scheduled_changes": {
    "target_plan": "free",
    "effective_date": "2025-02-15",
    "reason": "Reducing business size",
    "scheduled_at": "2025-01-20T10:30:00Z"
  },
  "created_at": "2025-01-01T00:00:00Z",
  "updated_at": "2025-01-20T10:30:00Z"
}

Important:

  • Downgrades are NOT immediate (prevent revenue loss)
  • Applied at current_period_end
  • No refund for remaining period
  • Can be canceled before effective date

Usage Tracking

Monitor current usage against subscription plan limits.

Endpoint

GET /api/v1/subscriptions/usage

Authentication: Required (JWT token)

Response

{
  "subscription_id": "67890abcdef1234567890123",
  "plan": "PRO",
  "billing_period": {
    "start": "2025-09-07T00:00:00Z",
    "end": "2025-10-07T00:00:00Z"
  },
  "usage": {
    "outlets": {
      "current": 3,
      "limit": 10,
      "percentage": 30,
      "status": "within_limit"
    },
    "staff": {
      "current": 12,
      "limit": 50,
      "percentage": 24,
      "status": "within_limit"
    },
    "appointments_this_month": {
      "current": 234,
      "limit": 2000,
      "percentage": 11.7,
      "status": "within_limit"
    },
    "services": {
      "current": 45,
      "limit": -1,
      "percentage": 0,
      "status": "unlimited"
    }
  },
  "warnings": [],
  "upgrade_recommended": false
}

Status Values:

  • within_limit - Usage below limit
  • approaching_limit - Usage > 80% of limit
  • at_limit - Usage at 100% of limit
  • exceeded - Usage over limit (blocked)
  • unlimited - No limit for this resource

Cancel Subscription

Cancel subscription by immediately downgrading to FREE plan.

Endpoint

POST /api/v1/subscriptions/cancel

Authentication: Required (JWT token)

Request Body

No request body required.

Response

Returns subscription downgraded to FREE:

{
  "id": "507f1f77bcf86cd799439011",
  "tenant_id": "507f1f77bcf86cd799439010",
  "plan": "free",
  "status": "active",
  "billing_period": "monthly",
  "current_period_start": "2025-01-15",
  "current_period_end": "2025-02-15",
  "metadata": {
    "cancelled_at": "2025-01-20T10:30:00Z",
    "cancelled_by": "user_id_here",
    "previous_plan": "pro"
  },
  "created_at": "2025-01-01T00:00:00Z",
  "updated_at": "2025-01-20T10:30:00Z"
}

Important:

  • Immediately downgrades to FREE plan
  • User keeps access with FREE tier limits
  • Status stays active (not blocked)
  • Previous plan stored in metadata for reference
  • Cannot cancel if already on FREE plan

Deactivate Subscription

Suspend a subscription (admin action for edge cases).

Endpoint

POST /api/v1/subscriptions/deactivate

Authentication: Required (JWT token)

Request Body

{
  "reason": "Payment fraud detected"
}

Parameters:

  • reason (optional) - Reason for deactivation

Response

Returns suspended subscription:

{
  "id": "507f1f77bcf86cd799439011",
  "tenant_id": "507f1f77bcf86cd799439010",
  "plan": "pro",
  "status": "suspended",
  "billing_period": "monthly",
  "metadata": {
    "deactivated_at": "2025-01-20T10:30:00Z",
    "deactivated_by": "admin_user_id",
    "deactivation_reason": "Payment fraud detected"
  },
  "created_at": "2025-01-01T00:00:00Z",
  "updated_at": "2025-01-20T10:30:00Z"
}

Use Cases:

  • Admin manually suspending a tenant
  • Payment fraud detection
  • Terms of service violation
  • Temporary account suspension

Important:

  • Plan and features are preserved
  • Access is blocked (status = suspended)
  • Can be reactivated using /activate endpoint

Activate Subscription

Reactivate a suspended subscription.

Endpoint

POST /api/v1/subscriptions/activate

Authentication: Required (JWT token)

Request Body

No request body required.

Response

Returns activated subscription:

{
  "id": "507f1f77bcf86cd799439011",
  "tenant_id": "507f1f77bcf86cd799439010",
  "plan": "pro",
  "status": "active",
  "billing_period": "monthly",
  "metadata": {
    "activated_at": "2025-01-21T09:00:00Z",
    "activated_by": "admin_user_id"
  },
  "created_at": "2025-01-01T00:00:00Z",
  "updated_at": "2025-01-21T09:00:00Z"
}

Use Cases:

  • Reactivating after payment issue resolved
  • Admin manually reactivating a tenant
  • Lifting suspensions after investigation

Important:

  • Restores access immediately
  • Plan and features unchanged
  • Clears deactivation metadata

Payment History

Retrieve all subscription-related payments.

Endpoint

GET /api/v1/subscriptions/payments?limit=20&offset=0&status=completed

Authentication: Required (JWT token)

Query Parameters

  • limit (optional) - Results per page (default: 20, max: 100)
  • offset (optional) - Pagination offset (default: 0)
  • status (optional) - Filter by status: completed, pending, failed, refunded

Response

Returns array of payment records:

[
  {
    "id": "507f1f77bcf86cd799439020",
    "tenant_id": "507f1f77bcf86cd799439010",
    "invoice_id": "507f1f77bcf86cd799439012",
    "subscription_id": "507f1f77bcf86cd799439011",
    "amount": 499900,
    "currency": "IDR",
    "status": "completed",
    "payment_type": "subscription_renewal",
    "payment_method": "paper_id",
    "paper_invoice_id": "PI-20250115-ABC123",
    "paid_at": "2025-01-15T10:30:00Z",
    "created_at": "2025-01-15T10:25:00Z",
    "metadata": {
      "plan": "pro",
      "billing_period": "monthly"
    }
  },
  {
    "id": "507f1f77bcf86cd799439021",
    "tenant_id": "507f1f77bcf86cd799439010",
    "invoice_id": "507f1f77bcf86cd799439013",
    "subscription_id": "507f1f77bcf86cd799439011",
    "amount": 249950,
    "currency": "IDR",
    "status": "completed",
    "payment_type": "subscription_upgrade",
    "payment_method": "paper_id",
    "paper_invoice_id": "PI-20250101-DEF456",
    "paid_at": "2025-01-01T14:20:00Z",
    "created_at": "2025-01-01T14:15:00Z",
    "metadata": {
      "from_plan": "free",
      "to_plan": "pro",
      "prorated": true
    }
  }
]

Webhook Architecture

The platform uses Paper.id as the payment gateway, which has a limitation: only one webhook URL per account.

Problem

With multiple invoice types (subscription upgrades, renewals, appointments), we need to route webhooks to different handlers but can only register one endpoint.

Solution: Single Endpoint with Intelligent Routing

Webhook Endpoint: POST /api/v1/webhooks/paper-invoice

All Paper.id webhook events are sent to this single endpoint, which then routes internally based on:

  1. Invoice Type (invoice_type field)
  2. Metadata Flags (custom data in invoice)

Routing Logic

async def handle_paper_invoice_webhook(payload: dict):
    """
    Single webhook endpoint that routes to appropriate handler
    """
    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: Period extends, plan stays same
            await handle_invoice_payment_for_renewal(invoice_data)
        else:
            # Upgrade: Plan changes, period stays same
            await handle_invoice_payment_for_subscription(invoice_data)

    elif invoice_type == "APPOINTMENT":
        # Appointment booking payment
        await handle_invoice_payment_for_appointment(invoice_data)

    else:
        # Unknown invoice type
        logger.warning(f"Unknown invoice type: {invoice_type}")
        raise HTTPException(status_code=400, detail="Unknown invoice type")

Invoice Metadata Structure

Subscription Upgrade Invoice:

{
  "invoice_type": "SUBSCRIPTION",
  "metadata": {
    "tenant_id": "12345abcdef67890abcdef12",
    "subscription_id": "67890abcdef1234567890123",
    "previous_plan": "BASIC",
    "new_plan": "PRO"
    // NO renewal flag
  }
}

Subscription Renewal Invoice:

{
  "invoice_type": "SUBSCRIPTION",
  "metadata": {
    "tenant_id": "12345abcdef67890abcdef12",
    "subscription_id": "67890abcdef1234567890123",
    "renewal": true,  // KEY: Renewal flag present
    "billing_cycle": "monthly"
  }
}

Appointment Invoice:

{
  "invoice_type": "APPOINTMENT",
  "metadata": {
    "tenant_id": "12345abcdef67890abcdef12",
    "appointment_id": "98765fedcba09876543210ab",
    "customer_id": "11111aaaaa22222bbbbb33333"
  }
}

Webhook Handlers

1. Upgrade Handler (handle_invoice_payment_for_subscription)

Triggered when: invoice_type == "SUBSCRIPTION" AND no renewal flag

Actions:

  1. Verify payment status is paid
  2. Extract subscription_id from metadata
  3. Update subscription:
  4. Change plan_type to new_plan
  5. Keep current_period_end unchanged
  6. Set status to active
  7. Update invoice status to paid
  8. Send upgrade confirmation email
  9. Log subscription change event

2. Renewal Handler (handle_invoice_payment_for_renewal)

Triggered when: invoice_type == "SUBSCRIPTION" AND renewal == true

Actions:

  1. Verify payment status is paid
  2. Extract subscription_id and billing_cycle from metadata
  3. Calculate new period end date:
  4. Monthly: +30 days
  5. Quarterly: +90 days
  6. Yearly: +365 days
  7. Update subscription:
  8. Keep plan_type unchanged
  9. Extend current_period_end by billing cycle
  10. Update current_period_start to old end date
  11. Set next_billing_date to new end date
  12. Update invoice status to paid
  13. Send renewal confirmation email
  14. Log renewal event

3. Appointment Handler (handle_invoice_payment_for_appointment)

Triggered when: invoice_type == "APPOINTMENT"

Actions:

  1. Verify payment status is paid
  2. Extract appointment_id from metadata
  3. Update appointment status to confirmed
  4. Update merchant balance (deposit payment - platform fee)
  5. Send booking confirmation to customer
  6. Send booking notification to staff
  7. Log payment event

Webhook Security

Important: Paper.id does NOT provide webhook signature verification (no HMAC signing).

Security Measures:

  1. Idempotency Checks - Track processed invoice IDs to prevent duplicate processing
  2. Invoice Verification - Validate invoice exists in database before processing
  3. Amount Verification - Confirm payment amount matches invoice amount
  4. Tenant Isolation - Ensure invoice belongs to correct tenant
  5. IP Whitelisting - Only accept webhooks from Paper.id IP ranges (production)
# Idempotency check
processed_invoices = set()  # In production: use Redis

async def handle_paper_invoice_webhook(payload: dict):
    invoice_id = payload['data']['invoice_id']

    # Check if already processed
    if invoice_id in processed_invoices:
        logger.info(f"Invoice {invoice_id} already processed, skipping")
        return {"status": "already_processed"}

    # Process webhook
    await route_to_handler(payload)

    # Mark as processed
    processed_invoices.add(invoice_id)

Testing Subscription Flows

Prerequisites

  1. ngrok Setup - Expose local server to receive webhooks
ngrok http 8000
  1. Paper.id Webhook Configuration

  2. Log in to Paper.id dashboard

  3. Navigate to Settings → Webhooks
  4. Set webhook URL: https://your-ngrok-url.ngrok.io/api/v1/webhooks/paper-invoice
  5. Save configuration

  6. Test Tenant - Create tenant with active subscription

Testing Renewal Flow

Scenario: Extend PRO Monthly subscription for another month

  1. API Call:
curl -X POST http://localhost:8000/api/v1/subscriptions/renew \
  -H "Authorization: Bearer YOUR_JWT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "subscription_id": "507f1f77bcf86cd799439011"
  }'
  1. Expected Response:

    {
      "status": "payment_pending",
      "invoice": {
        "paper_payment_url": "https://payper.id/short456",
        "amount": "499900"
      },
      "renewal_details": {
        "next_period_start": "2025-02-15",
        "next_period_end": "2025-03-15"
      }
    }
    

  2. Payment Simulation:

  3. Open payment_url in browser
  4. Complete payment using Paper.id test credentials
  5. Paper.id sends webhook to your ngrok URL

  6. Verify Webhook Routing:

  7. Check server logs for: "Processing renewal webhook for invoice PI-..."

  8. Verify renewal: true in metadata

  9. Verify Subscription Update:

curl -X GET http://localhost:8000/api/v1/subscriptions/current \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

Expected: current_period_end extended by 30 days

Testing Upgrade Flow

Scenario: Upgrade from FREE to PRO mid-cycle

  1. API Call:

    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
      }'
    

  2. Expected Response:

    {
      "status": "payment_pending",
      "upgrade_details": {
        "from_plan": "free",
        "to_plan": "pro",
        "prorated_amount": "249950",
        "days_remaining": 15,
        "total_days": 30
      },
      "invoice": {
        "paper_payment_url": "https://payper.id/short123",
        "amount": "249950"
      }
    }
    

  3. Payment Simulation:

  4. Complete payment via payment_url

  5. Paper.id sends webhook (NO renewal flag in metadata)

  6. Verify Webhook Routing:

  7. Check logs for: "Processing upgrade webhook for invoice PI-..."

  8. Verify NO renewal key in metadata

  9. Verify Subscription Update:

curl -X GET http://localhost:8000/api/v1/subscriptions/current \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

Expected:

  • plan_type: "ENTERPRISE"
  • current_period_end UNCHANGED (same expiry date)

Troubleshooting

Webhook Not Received

Symptoms: Payment completed but subscription not updated

Checks:

  1. Verify ngrok is running: ngrok http 8000
  2. Check Paper.id webhook URL is correct
  3. Review ngrok web interface: http://localhost:4040 for webhook requests
  4. Check server logs for webhook processing errors

Fix:

  • Manually trigger webhook from Paper.id dashboard (Resend Webhook button)
  • Or manually update subscription via admin endpoint

Wrong Handler Invoked

Symptoms: Renewal processed as upgrade (or vice versa)

Checks:

  1. Inspect invoice metadata in database:
    db.invoices.find_one({"paper_invoice_id": "PI-..."})
    
  2. Verify renewal flag is set correctly
  3. Check webhook routing logic

Fix:

  • Revert subscription to previous state
  • Correct metadata in invoice
  • Resend webhook from Paper.id

Prorated Amount Incorrect

Symptoms: Upgrade charge doesn't match expected prorated amount

Checks:

  1. Verify current_period_end date is correct
  2. Check days remaining calculation
  3. Review plan prices in database

Fix:

  • Recalculate proration manually
  • Issue refund/credit if overcharged
  • Update invoice amount

Duplicate Processing

Symptoms: Webhook processed multiple times, subscription period extended twice

Checks:

  1. Verify idempotency checks are working
  2. Check Redis/cache for processed invoice IDs
  3. Review webhook retry logs from Paper.id

Fix:

  • Revert duplicate changes manually
  • Strengthen idempotency logic
  • Add database transaction locks

Best Practices

For Upgrades

DO:

  • Always calculate prorated amounts server-side (never trust client)
  • Validate new plan is different from current plan
  • Set invoice expiration (7 days recommended)
  • Send email with payment link
  • Wait for webhook confirmation before activating upgrade

DON'T:

  • Apply upgrade immediately without payment
  • Allow upgrade to same plan (use renewal instead)
  • Skip proration calculation
  • Trust client-provided amounts

For Renewals

DO:

  • Charge full period amount (no proration)
  • Set renewal: true in invoice metadata
  • Extend period by exact billing cycle duration (30/90/365 days)
  • Allow renewal even if subscription hasn't expired yet (early renewal)
  • Send renewal reminder emails 7 days before expiry

DON'T:

  • Prorate renewal charges
  • Change plan during renewal
  • Allow renewal of canceled subscriptions
  • Process renewal without renewal flag in metadata

For Webhooks

DO:

  • Implement idempotency checks (prevent duplicate processing)
  • Validate invoice amounts match expected values
  • Log all webhook payloads for debugging
  • Return 200 OK quickly (process asynchronously if needed)
  • Use metadata routing flags consistently

DON'T:

  • Process webhook without idempotency check
  • Trust webhook without verification
  • Perform long-running operations in webhook handler
  • Fail to log webhook errors

API Reference Summary

Endpoint Method Purpose Key Response
/subscriptions/plans GET List all available plans Plans with pricing and limits
/subscriptions/current GET Get current subscription Full subscription details
/subscriptions/upgrade POST Change plan (prorated) Invoice with payment URL
/subscriptions/renew POST Extend period (full charge) Invoice with payment URL
/subscriptions/downgrade POST Schedule downgrade Effective date
/subscriptions/usage GET Monitor usage vs limits Usage percentages
/subscriptions/cancel POST Downgrade to FREE immediately FREE plan subscription
/subscriptions/deactivate POST Suspend subscription Suspended status
/subscriptions/activate POST Reactivate suspended subscription Active status
/subscriptions/payments GET Payment history List of invoices

Next Steps:

  1. Review available plans: GET /subscriptions/plans
  2. Check current subscription: GET /subscriptions/current
  3. Test upgrade flow with prorated billing
  4. Test renewal flow with full period charge
  5. Monitor usage to optimize plan selection

For webhook integration details, refer to the Webhook Routing Architecture section above.