Skip to content

Checking access...

Oracle-Bridge Renewal API ​

Story: 2.3.5 - Renewal Payment with Stripe Status: Backend Complete (Frontend Deferred to Epic 2.4) Last Updated: 2025-11-30

Overview ​

The Oracle-Bridge Renewal API provides backend endpoints for processing membership renewals via Stripe payment integration. This API supports both mock Stripe mode (for testing) and production Stripe mode.

Current Mode: Mock Stripe (USE_MOCK_STRIPE=true) Production Stripe: Blocked pending tax ID acquisition (Epic 2.5)

Base URL ​

  • Local Development: http://localhost:3000
  • Testnet: https://staging.helloworlddao.com/api
  • Mainnet: https://www.helloworlddao.com/api

Authentication ​

All renewal endpoints require valid user authentication. The user must have an active or expired (within grace period) membership to initiate renewal.

Endpoints ​

POST /api/create-renewal-checkout ​

Creates a Stripe checkout session for membership renewal.

Request ​

http
POST /api/create-renewal-checkout
Content-Type: application/json

{
  "user_id": "rrkah-fqaaa-aaaaa-aaaaq-cai",
  "amount": 2500
}

Request Body:

FieldTypeRequiredDescription
user_idstringYesIC Principal of the user renewing membership
amountnumberYesAmount in cents (must be exactly 2500 for $25.00)

Response ​

Success (200 OK):

json
{
  "session_id": "cs_test_a1b2c3d4e5f6...",
  "checkout_url": "https://checkout.stripe.com/pay/cs_test_...",
  "amount": 2500,
  "expires_at": 1732574400
}

Response Body:

FieldTypeDescription
session_idstringStripe checkout session ID
checkout_urlstringURL to redirect user to Stripe checkout
amountnumberRenewal amount in cents
expires_atnumberUnix timestamp when session expires

Error Responses:

json
// 400 Bad Request - Invalid amount
{
  "error": "Invalid renewal amount. Expected $25.00 (2500 cents)."
}

// 403 Forbidden - Cannot renew
{
  "error": "You are not eligible for renewal at this time."
}

// 404 Not Found - Membership not found
{
  "error": "Membership not found for user."
}

Flow Diagram ​

Frontend                    Oracle-Bridge                Membership Canister
    |                              |                              |
    |-- POST /create-renewal ----->|                              |
    |                              |-- can_renew(principal) ----->|
    |                              |<-- Ok/Err -------------------|
    |                              |                              |
    |                              |-- Create Stripe session -----|
    |<-- checkout_url -------------|                              |
    |                              |                              |
    |-- Redirect to Stripe ------->| (Stripe hosted page)         |

Renewal Window ​

Membership renewals are only allowed during the renewal window:

  • Start: December 1st
  • End: January 31st (11:59 PM)

Attempting to renew outside this window will result in a 403 Forbidden error.

Grace Period ​

Members whose membership expires on December 31st have a grace period until January 31st to renew:

  • Membership Status: Changes from Active → Expired on January 1st
  • Renewal Eligibility: Members can still renew during grace period (Jan 1 - Jan 31)
  • Post-Grace: After January 31st, expired members must re-apply as new members

Webhook Processing (Internal) ​

Note: This webhook is handled internally by the oracle-bridge service and is not exposed to the frontend.

When Stripe checkout is completed, Stripe sends a webhook to oracle-bridge:

POST /webhooks/stripe

Webhook Flow:

  1. Stripe sends checkout.session.completed event
  2. Oracle-bridge verifies webhook signature
  3. Parses session metadata (type: "renewal", user_id: "...")
  4. Generates payment proof
  5. Calls membership canister: renew_membership(principal, payment_proof)
  6. Records payment to treasury canister (fire-and-forget)
  7. Sends email receipt to user (fire-and-forget)

Payment Proof Format:

typescript
{
  payment_id: string;       // Stripe payment intent ID
  amount: number;           // Amount in cents
  timestamp: bigint;        // Nanosecond timestamp
  provider: 'stripe';
}

Payment History ​

After successful renewal, the payment is recorded in the treasury canister and can be queried by the frontend.

Treasury API (via IC Canister) ​

typescript
// Query payment history
treasury.get_payment_history_with_session(
  session_token: string,
  limit: number,
  offset: number,
  payment_type_filter: { Renewal: null } | null,
  start_date: bigint | null,
  end_date: bigint | null
) => Result<PaymentRecord[], string>

// PaymentRecord structure
{
  id: bigint;
  user: Principal;
  amount: Nat;
  payment_type: { Initial: null } | { Renewal: null };
  payment_intent_id: string;
  payment_method_last4: string;
  receipt_number: string | null;
  timestamp: bigint;
}

Frontend Usage Example:

typescript
import { Actor } from '@dfinity/agent';
import { treasuryIdlFactory } from './treasury.did.js';

// Get treasury canister actor
const treasury = Actor.createActor(treasuryIdlFactory, {
  agent,
  canisterId: process.env.TREASURY_CANISTER_ID
});

// Query renewal payments only
const result = await treasury.get_payment_history_with_session(
  userSessionToken,
  10,  // limit
  0,   // offset
  { Renewal: null },  // filter for renewals only
  null,  // start_date
  null   // end_date
);

if ('Ok' in result) {
  const renewalPayments = result.Ok;
  // Display payment history to user
}

Email Receipts ​

After successful renewal, users receive an email receipt with the following information:

  • Subject: "Membership Renewal Receipt - Hello World DAO"
  • Content:
    • Renewal amount: $25.00
    • Payment date
    • Transaction ID (Stripe payment intent ID)
    • Updated membership expiration date (next December 31st)
    • Receipt number (format: REN-{timestamp}-{hash})

GDPR Compliance: Users who have opted out of email receipts (email_receipts = false in user profile) will NOT receive email receipts. Renewal will still succeed.

Error Handling ​

Client-Side Error Handling ​

The frontend should handle these error scenarios:

  1. Invalid Renewal Amount

    • Validate that amount is exactly 2500 cents before calling API
  2. Renewal Window Closed

    • Display message: "Membership renewals are only available from December 1st to January 31st"
    • Provide option to apply as new member after grace period
  3. Revoked Membership

    • Display message: "Your membership has been revoked. You must apply as a new member."
  4. Payment Failure

    • User is redirected to cancel URL with error parameter
    • Display payment error and offer to retry
  5. Network Errors

    • Implement retry logic with exponential backoff
    • Display user-friendly error message

Backend Error Handling ​

Oracle-bridge implements fire-and-forget pattern for non-critical operations:

  • Email send failures: Logged but do not block renewal
  • Treasury recording failures: Logged but do not block renewal
  • Membership canister failures: Return error to frontend (critical)

Testing ​

Mock Stripe Mode ​

Current implementation uses mock Stripe for testing:

Test Cards:

  • Success: 4242424242424242
  • Declined: 4000000000000002
  • Insufficient Funds: 4000000000009995

Mock Webhook:

Mock Stripe automatically triggers webhook after successful payment in test mode.

Frontend Integration Testing ​

To test renewal flow in development:

typescript
// 1. Create renewal checkout
const checkoutResponse = await fetch('http://localhost:3000/api/create-renewal-checkout', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    user_id: 'rrkah-fqaaa-aaaaa-aaaaq-cai',
    amount: 2500
  })
});

const { checkout_url } = await checkoutResponse.json();

// 2. Redirect user to checkout
window.location.href = checkout_url;

// 3. User completes payment on Stripe (or mock) page

// 4. User is redirected back to success URL

// 5. Query membership canister to verify renewal
const membership = await membershipActor.get_membership(userPrincipal);
console.log('New expiration:', membership.expiration_date);

// 6. Query treasury to see payment history
const payments = await treasuryActor.get_payment_history_with_session(
  sessionToken, 10, 0, { Renewal: null }, null, null
);

Security Considerations ​

  1. Amount Validation: Server validates that amount is exactly $25.00
  2. Principal Verification: User must be authenticated and principal must match session
  3. Renewal Eligibility: Server checks can_renew() before creating checkout
  4. Webhook Signature: Stripe webhook signature is verified (production mode)
  5. Payment Proof: Payment proof includes cryptographic signature (production - TODO)
  6. Idempotency: Duplicate webhook events are handled gracefully

Production Checklist ​

Before enabling production Stripe:

  • [ ] Obtain tax ID for Hello World DAO
  • [ ] Configure production Stripe account
  • [ ] Set USE_MOCK_STRIPE=false in environment
  • [ ] Configure Stripe webhook secret
  • [ ] Implement Ed25519 cryptographic signature for payment proof
  • [ ] Test with real payment methods
  • [ ] Verify email SMTP configuration for production
  • [ ] Update success/cancel URLs to production frontend URLs
  • Story 2.3.5: /home/coby/git/docs/sprints/stories/2-3-5-renewal-payment-stripe.md
  • Epic 2.3: Membership Lifecycle & Renewal
  • Epic 2.4: Payment Integration & Proration (Frontend UI)
  • Membership Canister API: /home/coby/git/membership/src/membership.did
  • Treasury Canister API: /home/coby/git/treasury/src/treasury.did

Support ​

For questions or issues:


Generated: 2025-11-30 Story: 2.3.5 - Renewal Payment with Stripe Backend Status: ✅ Complete & Tested (20/20 tests passing) Frontend Status: ⏸️ Deferred to Epic 2.4 (Stories 2-4-3a, 2-4-3b)

Hello World DAO