All offer API endpoints live under apps/zooly-app/app/api/offers/. They support CORS for cross-origin requests from zooly-app2.
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 JSON401 — Not authenticated404 — Account not found for slugImplementation: apps/zooly-app/app/api/offers/submit/route.ts
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 authenticatedImplementation: apps/zooly-app/app/api/offers/list/route.ts
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/counterNote401 — Not authenticated403 — Offer does not belong to authenticated seller404 — Offer not foundImplementation: apps/zooly-app/app/api/offers/respond/route.ts
Located in packages/db/src/access/offers.ts:
| Function | Description |
|---|---|
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 |