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).
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
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)
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
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
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
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.
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
Handles Stripe webhook events.
Endpoint: POST /api/payments/webhook/stripe
Auth: Webhook signature verification
Headers:
stripe-signature: Stripe webhook signatureBody: 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 FAILEDpayment_intent.payment_failed: Marks payment as FAILEDcharge.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.
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
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
All endpoints return standard error responses:
{
error: string; // Error message
code?: string; // Error code (optional)
}
Common Error Codes:
UNAUTHORIZED: Authentication requiredFORBIDDEN: Insufficient permissionsNOT_FOUND: Resource not foundBAD_REQUEST: Invalid request parametersINTERNAL_ERROR: Server errorgetVerifiedUserInfo())CRON_SECRET bearer token