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
POST /api/create-renewal-checkout
Content-Type: application/json
{
"user_id": "rrkah-fqaaa-aaaaa-aaaaq-cai",
"amount": 2500
}Request Body:
| Field | Type | Required | Description |
|---|---|---|---|
user_id | string | Yes | IC Principal of the user renewing membership |
amount | number | Yes | Amount in cents (must be exactly 2500 for $25.00) |
Response
Success (200 OK):
{
"session_id": "cs_test_a1b2c3d4e5f6...",
"checkout_url": "https://checkout.stripe.com/pay/cs_test_...",
"amount": 2500,
"expires_at": 1732574400
}Response Body:
| Field | Type | Description |
|---|---|---|
session_id | string | Stripe checkout session ID |
checkout_url | string | URL to redirect user to Stripe checkout |
amount | number | Renewal amount in cents |
expires_at | number | Unix timestamp when session expires |
Error Responses:
// 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→Expiredon 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/stripeWebhook Flow:
- Stripe sends
checkout.session.completedevent - Oracle-bridge verifies webhook signature
- Parses session metadata (
type: "renewal", user_id: "...") - Generates payment proof
- Calls membership canister:
renew_membership(principal, payment_proof) - Records payment to treasury canister (fire-and-forget)
- Sends email receipt to user (fire-and-forget)
Payment Proof Format:
{
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)
// 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:
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:
Invalid Renewal Amount
- Validate that amount is exactly 2500 cents before calling API
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
Revoked Membership
- Display message: "Your membership has been revoked. You must apply as a new member."
Payment Failure
- User is redirected to cancel URL with error parameter
- Display payment error and offer to retry
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:
// 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
- Amount Validation: Server validates that amount is exactly $25.00
- Principal Verification: User must be authenticated and principal must match session
- Renewal Eligibility: Server checks
can_renew()before creating checkout - Webhook Signature: Stripe webhook signature is verified (production mode)
- Payment Proof: Payment proof includes cryptographic signature (production - TODO)
- 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=falsein 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
Related Documentation
- 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:
- GitHub Issues: https://github.com/Hello-World-Co-Op/hello-world-workspace/issues
- Technical Contact: dev@helloworlddao.com
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)