API Reference

API endpoints for the payment system

Overview

All payment API endpoints are located in apps/zooly-app/app/api/payments/. API routes are thin layers that handle parameter extraction, authentication, and delegate to the service layer (@zooly/srv-stripe-payment).

Endpoints

Create Payment Intent

Creates a Stripe PaymentIntent. The backend fetches the product from the product table (source of truth for price and seller).

Endpoint: POST /api/payments/create-intent

Auth: Required (or guest checkout)

Request Body:

{
  payFor: "MERCH" | "VOICE_OVER" | "LIKENESS" | "IMAGE" | "OFFER";
  payForId: string;               // FK to product-specific row (required)
  buyerName?: string;              // Optional buyer name
  buyerEmail?: string;             // Optional buyer email
  hostPartnerSlug?: string;        // Optional host partner slug
}

Note: sellerAccountId and amountCent are NOT accepted from the client. The backend fetches these from the product table (or equivalent resolver) via getProductByPayForId() to prevent tampering.

For payFor: "OFFER", payForId is the offer row id (offers.id). The brand pays after the offer is ACCEPTED. Charge completion moves the offer to PAID and holds revenue in escrow (no payment_share_tracking rows yet). Share tracking is created when the buyer accepts delivery via releaseOfferEscrowForOffer() (see Payment Flows).

Response:

{
  stripePaymentId: string;       // Internal payment ID
  stripeClientSecret: string;     // Stripe.js client secret
  stripePublishableKey: string;   // Stripe publishable key
}

Implementation: apps/zooly-app/app/api/payments/create-intent/route.ts

Service Function: createPaymentIntent() in @zooly/srv-stripe-payment

Payment Completion

Completes a payment by querying the Stripe API to verify the charge succeeded, then calling the completion engine. This is the primary path for payment completion; the webhook serves as a fallback.

Endpoint: POST /api/payments/complete

Auth: Required

Request Body:

{
  stripePaymentId: string;
}

Response (200 - completed):

{
  ppuCode: string;
  stripePayment: StripePayment;  // sanitized, without internal fields
}

Response (202 - still processing):

{
  error: string;
  stillProcessing: true;
}

The 202 response means the PaymentIntent hasn't succeeded at Stripe yet. The UI should retry after a short delay.

Implementation: apps/zooly-app/app/api/payments/complete/route.ts

Service Function: completePaymentFromClient() in @zooly/srv-stripe-payment (which calls completePayment() internally)

Payment History

Lists payments for an account (buyer or seller).

Endpoint: GET /api/payments/history

Auth: Required

Query Parameters:

  • accountId: Account ID (defaults to authenticated user's account)
  • role: "buyer" | "seller" (optional)
  • limit: Number of results (default: 20)
  • offset: Pagination offset (default: 0)

Response:

{
  payments: StripePayment[];
  total: number;
}

Implementation: apps/zooly-app/app/api/payments/history/route.ts

Service Function: listStripePaymentsByAccount() in @zooly/app-db

Payout Status

Lists payouts for an account.

Endpoint: GET /api/payments/payout-status

Auth: Required

Query Parameters:

  • accountId: Account ID (defaults to authenticated user's account)

Response:

{
  payouts: PayOut[];
  openTrackingSum: number;        // Total OPEN tracking in cents
  minimumPayoutAmount: number;     // Threshold in cents
}

Implementation: apps/zooly-app/app/api/payments/payout-status/route.ts

Service Function: listPayoutsByAccount(), getOpenTrackingSumByAccount() in @zooly/app-db

Payout Route

Gets or updates payout route configuration for an account.

Endpoint: GET /api/payments/payout-route Endpoint: POST /api/payments/payout-route

Auth: Required

GET Response:

{
  payoutRoute: AccountPayoutRoute | null;
}

POST Request Body:

{
  stripeConnectAccountId?: string;
  kycVerified?: boolean;
}

POST Response:

{
  payoutRoute: AccountPayoutRoute;
}

Implementation: apps/zooly-app/app/api/payments/payout-route/route.ts

Service Function: getPayoutRouteByAccountId(), upsertPayoutRoute() in @zooly/app-db

Refund

Processes a full refund for a completed payment.

Endpoint: POST /api/payments/refund

Auth: Admin required

Request Body:

{
  stripePaymentId: string;
}

Response:

{
  success: boolean;
  refundStripePaymentId: string;
  stripeRefundId: string;         // re_xxx from Stripe
}

Implementation: apps/zooly-app/app/api/payments/refund/route.ts

Service Function: processRefund() in @zooly/srv-stripe-payment

Note: Partial refunds are NOT supported. Only full refunds are allowed.

Process Payouts (Cron)

Daily cron job that processes payouts for accounts meeting the threshold.

Endpoint: GET /api/payments/process-payouts

Auth: CRON_SECRET bearer token

Schedule: Daily at 2 PM UTC (0 14 * * *)

Response:

{
  success: boolean;
  message: string;
  processedCount: number;
  skippedCount: number;
  errors: { accountId: string; error: string }[];
}

Implementation: apps/zooly-app/app/api/payments/process-payouts/route.ts

Service Function: payoutDaemon() in @zooly/srv-stripe-payment

Stripe Webhook

Handles Stripe webhook events.

Endpoint: POST /api/payments/webhook/stripe

Auth: Webhook signature verification

Headers:

  • stripe-signature: Stripe webhook signature

Body: Raw Stripe event JSON

Handled Events:

  • charge.succeeded: Calls completePayment() as a fallback (primary completion is via POST /api/payments/complete)
  • charge.failed: Marks payment as FAILED
  • payment_intent.payment_failed: Marks payment as FAILED
  • charge.refunded: TODO - External refund handling (not yet implemented)

Implementation: apps/zooly-app/app/api/payments/webhook/stripe/route.ts

Service Function: verifyWebhookSignature(), completePayment() in @zooly/srv-stripe-payment

Note: Webhook signature verification is STRICT - if STRIPE_WEBHOOK_SECRET is missing, all webhooks are rejected. The webhook uses the same idempotent completePayment() function as the client endpoint — if the client already completed the payment, the webhook is a no-op.

Admin Endpoints

Manual Payout

Manually trigger a payout for a specific account (admin only).

Endpoint: POST /api/payments/admin/do-payout

Auth: Admin required

Request Body:

{
  accountId: string;
  dryRun?: boolean;               // If true, returns projected payout without executing
}

Response:

{
  status: "success" | "below_threshold" | "error";
  amount?: number;                // Payout amount in cents
  transferId?: string;            // Stripe transfer ID (tr_xxx)
  error?: string;
}

Implementation: apps/zooly-app/app/api/payments/admin/do-payout/route.ts

Service Function: doPayout() in @zooly/srv-stripe-payment

Advance Payout

Pre-pay talent before earnings accumulate (admin only).

Endpoint: POST /api/payments/admin/advance-payout

Auth: Admin required

Request Body:

{
  accountId: string;
  amountCent: number;
}

Response:

{
  payOut: PayOut;
  stripeTransferId: string;       // tr_xxx from Stripe
}

Implementation: apps/zooly-app/app/api/payments/admin/advance-payout/route.ts

Service Function: processAdvancePayout() in @zooly/srv-stripe-payment

Error Responses

All endpoints return standard error responses:

{
  error: string;                  // Error message
  code?: string;                  // Error code (optional)
}

Common Error Codes:

  • UNAUTHORIZED: Authentication required
  • FORBIDDEN: Insufficient permissions
  • NOT_FOUND: Resource not found
  • BAD_REQUEST: Invalid request parameters
  • INTERNAL_ERROR: Server error

Authentication

  • User Endpoints: Require valid JWT token (via getVerifiedUserInfo())
  • Admin Endpoints: Require admin role in addition to authentication
  • Cron Endpoints: Require CRON_SECRET bearer token
  • Webhook Endpoints: Require valid Stripe webhook signature

Next Steps