Tiago Fortunato
ProjectsOdysUser Journeys

Payment Flows

PIX flow (client → pro, off-Stripe) + Stripe subscription flow (pro → Odys)

Payment Flows

Odys manages two distinct payment flows, each serving a different relationship within the platform: payments from clients to professionals for services rendered, and subscription payments from professionals to Odys for platform access. Understanding these mechanisms is crucial for comprehending how value is exchanged and how the platform's business model operates.

Overview

The Odys platform facilitates financial transactions through two primary channels. First, clients pay professionals directly for appointments, primarily using Brazil's instant payment system, PIX. This flow is largely "off-Stripe," meaning Odys provides the tools for PIX generation but does not intermediate the funds. Second, professionals subscribe to Odys's services, paying a recurring fee to access various features. This subscription model is handled entirely through Stripe, a third-party payment processor, ensuring automated billing and plan management.

The core database schema reflects these payment structures. The professionals table is central, storing details for both payment types: pixKeyType and pixKey for client payments, and stripeCustomerId, stripeSubscriptionId, plan, and trialEndsAt for their subscription to Odys. The appointments table includes a paymentStatus column, which is relevant for tracking client payments, regardless of the method.

Client-to-Professional Payments via PIX

For transactions between a client and a professional, Odys integrates with PIX, Brazil's instant payment system. This allows professionals to receive payments directly from their clients.

Professionals configure their PIX details within their profile, which are stored in the professionals table under the pixKeyType and pixKey columns. These fields are nullable, indicating that a professional might not offer PIX as a payment option or might not have configured it yet.

The src/lib/pix.ts file contains the logic for generating a PIX static QR code payload. The buildPixPayload function takes the professional's key, name, an optional city (defaulting to "Brasil"), and the amount of the payment (in cents) to construct a valid EMV BR Code. This function adheres to the Banco Central do Brasil's standards for PIX initiation. It uses helper functions like field to format data according to the PIX specification and crc16 to calculate the Cyclic Redundancy Check, ensuring the integrity of the generated payload.

When a client books an appointment, the system can generate a PIX QR code based on the professional's configured sessionPrice from the professionals table. The appointments table tracks the paymentStatus for each booking, which would typically be updated once the professional confirms receipt of the PIX payment. The "PIX integrado" feature is available even on the "Free" plan, as defined in src/lib/stripe/plans.ts, highlighting its foundational role in client-professional transactions.

Professional-to-Odys Payments via Stripe Subscriptions

Professionals subscribe to Odys's services through a tiered plan structure, managed by Stripe. This flow handles the recurring payments from professionals to Odys.

The available subscription plans—"Free", "Basic", "Pro", and "Premium"—are defined in src/lib/stripe/plans.ts within the PLANS object. Each plan specifies its label, price, priceId (referencing a Stripe Price ID), limits (e.g., clients, appointmentsPerMonth), and a list of features. The free plan also includes a note field for additional details. The PLAN_FEATURES array provides a master list of all features, used for comparison displays.

When a professional decides to upgrade their plan, the process is initiated through the POST /api/stripe/checkout API route.

  1. Authentication and Authorization: The route first verifies the user's identity using supabase.auth.getUser(). It also applies a rate limit using getApiLimiter() to prevent abuse.
  2. Plan Selection: The request body specifies the desired plan (e.g., "basic", "pro", "premium"). The system validates this against the PLANS object, rejecting requests for invalid or "free" plans.
  3. Professional Lookup: The system fetches the professional's details from the professionals table using their userId.
  4. Trial Management: If the professional is upgrading to the "pro" plan and still within their trial period (checked via trialDaysLeft), Stripe is instructed to defer payment collection until the trial ends. The trialEnd timestamp is passed to Stripe's session creation.
  5. Stripe Checkout Session: A Stripe Checkout Session is created using stripe.checkout.sessions.create. This session is configured for subscription mode, accepts card payments, and includes the priceId for the selected plan. Crucially, metadata containing professionalId and plan is attached to the session, which is vital for later reconciliation via webhooks. The success_url and cancel_url direct the user back to the Odys dashboard after the checkout process.

After a professional completes the checkout process on Stripe, or if their subscription status changes, Stripe sends events to the POST /api/stripe/webhook endpoint. This webhook is critical for keeping Odys's database synchronized with Stripe's subscription state.

  1. Signature Verification: The webhook first verifies the stripe-signature header to ensure the request genuinely originated from Stripe, preventing spoofing.
  2. checkout.session.completed: When a new subscription payment is successfully completed, this event is triggered. The system extracts professionalId and plan from the session's metadata. It then updates the professionals table, setting the plan, stripeCustomerId, and stripeSubscriptionId. Analytics events like subscription_started are also captured.
  3. customer.subscription.updated: If a professional changes their subscription (e.g., upgrades or downgrades) via the Stripe customer portal, this event updates the plan in the professionals table to reflect the new subscription tier based on the priceId of the active subscription item.
  4. customer.subscription.deleted: When a subscription is canceled, this event sets the professional's plan back to "free" and clears their stripeSubscriptionId in the professionals table.

This webhook-driven approach ensures that the professional's plan status within Odys is always consistent with their active Stripe subscription, enabling or disabling features accordingly.

Design Decisions

The payment architecture reflects a clear separation of concerns and leverages specialized third-party services where appropriate.

The decision to use PIX for client-to-professional payments, rather than an integrated payment gateway, stems from PIX's prevalence and ease of use in Brazil. By providing the buildPixPayload utility, Odys empowers professionals to accept payments directly without incurring platform fees on these transactions. This design choice prioritizes direct financial relationships between professionals and their clients, keeping Odys out of the money flow for services. The trade-off is that Odys has less direct control over the payment process and relies on professionals to manually confirm paymentStatus for appointments.

For professional subscriptions, Stripe was chosen for its comprehensive platform for recurring billing. This offloads the complexity of payment processing, subscription management, and tax compliance to a dedicated service. The use of webhooks (/api/stripe/webhook) is a fundamental design choice, ensuring that Odys's internal professionals table remains the single source of truth for subscription status, synchronized with Stripe. This asynchronous, event-driven approach is more resilient than polling and handles various subscription lifecycle events (creation, updates, cancellations) automatically. The PLANS object in src/lib/stripe/plans.ts centralizes all plan-related data, making it easy to manage and display subscription options consistently across the application.

The integration of trial periods, specifically for the "pro" plan, demonstrates a strategic decision to encourage adoption of higher-tier services by reducing initial commitment. The trialDaysLeft function and the conditional trial_end parameter in the Stripe Checkout Session creation (src/app/api/stripe/checkout/route.ts) are key to implementing this.

Potential Improvements

  1. Automate PIX Payment Confirmation: Currently, the PIX flow is off-platform, meaning appointments.paymentStatus likely requires manual updates by the professional. Implementing a mechanism to automatically confirm PIX payments would significantly enhance the user experience and reduce administrative burden. This could involve integrating with a PIX API that provides webhooks for payment notifications or developing a reconciliation process that periodically checks bank statements for incoming PIX transactions linked to appointments.
  2. Enforce Plan Limits: The PLANS object in src/lib/stripe/plans.ts defines limits such as clients and appointmentsPerMonth. However, the provided code does not show where these limits are actively enforced within the application logic. For example, when a professional on the "Free" plan attempts to add an 11th client or book a 21st appointment, the system should prevent the action and prompt an upgrade. Implementing this enforcement logic, perhaps in the /api/booking or /api/client-profile routes, is crucial for the integrity of the subscription model.
  3. Refine Stripe Webhook Error Handling: In src/app/api/stripe/webhook/route.ts, the logger.error("stripe_webhook_invalid_sig", { err }) call logs the raw error object. While useful for debugging, it might expose sensitive details in production logs. Consider refining the error logging to extract only relevant, non-sensitive information from the err object, or ensure that the logging infrastructure is configured to handle sensitive data appropriately.
  4. PIX Key Validation: The buildPixPayload function in src/lib/pix.ts accepts a key as a string without explicit validation of its format (e.g., CPF, CNPJ, email, phone). While the PIX system itself would reject an invalid key, adding client-side or server-side validation when the professional configures their pixKey in the professionals table would improve data quality and user experience.

References

  • src/lib/pix.ts
  • src/lib/stripe/plans.ts
  • src/app/api/stripe/checkout/route.ts
  • src/app/api/stripe/webhook/route.ts

On this page