Offers API Reference

API endpoints for the offers system

Overview

All offer API endpoints live under apps/zooly-app/app/api/offers/. They support CORS for cross-origin requests from zooly-app2.


Submit Offer (Authenticated)

Creates a new offer. The buyer is resolved from the authenticated user's account. The seller is resolved from the slug in the request body.

Endpoint: POST /api/offers/submit

Auth: Required (cookie-based; uses resolveAccountId to get buyerAccountId)

Request Body:

{
  slug: string;                    // Seller's Z-link slug (required)
  campaignDescription: string;     // Campaign description (required)
  campaignTypes?: string[];       // e.g. ["Radio ads", "Billboards"] (default: [])
  scriptText?: string;             // Radio ad script (optional)
  usageTypes?: string[];           // Where content will be used (default: [])
  customUsage?: string;           // Free-text usage (optional)
  wantsSharing?: boolean | null;  // Social sharing bundle (optional)
  sharingDescription?: string;    // Sharing details (optional)
  offerAmountCent: number;        // Price in cents (required)
}

Response (201):

{
  offerId: string;
}

Errors:

  • 400 — Missing required fields or invalid JSON
  • 401 — Not authenticated
  • 404 — Account not found for slug

Implementation: apps/zooly-app/app/api/offers/submit/route.ts


List Offers (Authenticated)

Returns all offers for the authenticated seller's account.

Endpoint: GET /api/offers/list

Auth: Required (cookie-based; uses resolveAccountId)

Request: No body; cookies sent automatically with credentials: 'include'

Response (200):

{
  offers: Offer[];
  buyerAccounts: Record<string, { displayName: string; imageUrl: string | null }>;
}

Offer shape:

{
  id: string;
  sellerAccountId: string;
  buyerAccountId: string;
  campaignDescription: string;
  campaignTypes: string[];
  scriptText: string | null;
  usageTypes: string[];
  customUsage: string | null;
  wantsSharing: boolean | null;
  sharingDescription: string | null;
  offerAmountCent: number;
  counterAmountCent: number | null;
  counterNote: string | null;
  status: "PENDING" | "ACCEPTED" | "REJECTED" | "COUNTERED";
  deletedAt: Date | null;
  createdAt: Date;
  updatedAt: Date;
}

The buyerAccounts map provides display info for each unique buyerAccountId referenced in the offers array, keyed by account ID.

Errors:

  • 401 — Not authenticated

Implementation: apps/zooly-app/app/api/offers/list/route.ts


Respond to Offer (Authenticated)

Updates an offer's status. Validates that the offer belongs to the authenticated seller.

Endpoint: POST /api/offers/respond

Auth: Required (cookie-based)

Request Body:

{
  offerId: string;                 // Required
  action: "accept" | "reject" | "counter";  // Required
  counterAmountCent?: number;     // Required when action === "counter"
  counterNote?: string;           // Required when action === "counter"
}

Response (200):

{
  offer: Offer;  // Updated offer
}

Errors:

  • 400 — Missing offerId/action, invalid action, or counter missing counterAmountCent/counterNote
  • 401 — Not authenticated
  • 403 — Offer does not belong to authenticated seller
  • 404 — Offer not found

Implementation: apps/zooly-app/app/api/offers/respond/route.ts


DB Access Layer

Located in packages/db/src/access/offers.ts:

FunctionDescription
createOffer(data: NewOffer)Insert offer with PENDING status
getOfferById(id: string)Fetch single offer (excludes soft-deleted)
listOffersBySellerAccount(sellerAccountId: string)List offers for seller, newest first
updateOfferStatus(id, status, counterData?)Update status; when COUNTERED, sets counterAmountCent and counterNote