Base URL:
https://piazzaviva.it(orhttp://localhost:3000in development)
All API routes live under /api/. Responses are JSON unless otherwise noted. Error responses follow the shape { error: string }.
- Authentication
- Public API
- Events
- Auth
- Attendance
- Tickets & Checkout
- Comments
- Calendar
- Venues
- AI
- Analytics
- Notifications
- Social
- Demand & Waitlist
- Organizer & Connect
- Verification
- Admin
- System
- Embed & OG
- Webhooks
- Rate Limits
PiazzaViva uses two authentication mechanisms:
| Method | Header/Cookie | Used By |
|---|---|---|
| Session cookie | pv_session (httpOnly) |
Browser clients (pages, internal APIs) |
| API key | X-API-Key header |
Public API, cron jobs |
| Admin API key | X-API-Key header |
Admin endpoints |
Session cookies are set automatically on login/register and expire after 30 days.
Read-only endpoints for external integrations. Require X-API-Key header. Rate limited at 1000 req/hour per key.
List published events with filtering and pagination.
| Parameter | Type | Default | Description |
|---|---|---|---|
city |
string | — | Filter by city name |
category |
string | — | Filter by category slug |
from |
ISO date | — | Start date (inclusive) |
to |
ISO date | — | End date (inclusive) |
limit |
number | 20 | Results per page (max 100) |
offset |
number | 0 | Pagination offset |
Response:
{
"data": [
{
"id": "clx...",
"title": "Jazz in Piazza",
"description": "...",
"startDate": "2025-07-15T20:00:00.000Z",
"endDate": null,
"venue": "Piazza Saffi",
"address": "Piazza Saffi, Forlì",
"city": "Forlì",
"category": "musica",
"organizer": "Comune di Forlì",
"price": null,
"imageUrl": "https://...",
"latitude": 44.222,
"longitude": 12.041,
"capacity": 500,
"url": "https://piazzaviva.it/events/clx..."
}
],
"pagination": {
"total": 42,
"limit": 20,
"offset": 0,
"hasMore": true
}
}List venues with optional city filter.
| Parameter | Type | Default | Description |
|---|---|---|---|
city |
string | — | Filter by city name |
List all events with optional category/date filters.
| Parameter | Type | Description |
|---|---|---|
category |
string | Category slug filter |
date |
string | Date filter |
Response: { events: EventRecord[], total: number }
Create a new event. Emits event.created domain event.
Body:
{
"title": "Concerto Jazz",
"description": "Una serata di jazz...",
"startDate": "2025-07-20T20:00:00Z",
"venue": "Teatro Diego Fabbri",
"address": "Corso Diaz 47, Forlì",
"city": "Forlì",
"category": "musica",
"organizer": "Jazz Club Forlì",
"organizerInfo": "Dal 1985...",
"price": 15,
"image": "concerto-jazz.jpg",
"capacity": 300
}Response: 201 { event: EventRecord }
Get a single event by ID with full details.
Update an event. Requires session (organizer or admin).
Cancel an event. Sets status to "cancelled" and emits event.cancelled (notifies all attendees via email).
Duplicate an event with a new start date.
Get event attendance counts and attendee list (visible only).
Export event as .ics calendar file.
Server-Sent Events (SSE) endpoint for real-time event updates.
Get community-submitted photos for an event.
Upload a photo for an event. Requires session.
Get or generate a post-event memory (summary, photo highlights, stats).
Full-text search for events.
| Parameter | Type | Description |
|---|---|---|
q |
string | Search query |
Track event view/impression for analytics.
Handles login, register, and logout via the action field.
Login:
{ "action": "login", "email": "user@example.com", "password": "secret123" }Register:
{ "action": "register", "email": "user@example.com", "password": "secret123", "name": "Mario Rossi" }Logout:
{ "action": "logout" }Responses:
- Login/Register:
{ user: { id, email, name } }(setspv_sessioncookie) - Logout:
{ ok: true }(clears cookie)
Get the currently authenticated user. Returns { user: null } if not logged in.
Response:
{
"user": {
"id": "clx...",
"email": "user@example.com",
"name": "Mario Rossi",
"role": "organizer",
"interests": "musica,cultura",
"bio": "..."
}
}Toggle attendance (going/interested) for an event. Requires session.
Body:
{
"eventId": "clx...",
"type": "going",
"visible": true,
"groupSize": 3
}- If the same type already exists, removes the attendance (toggle off)
visibleenables "who's going" display (only forgoingtype)groupSizeclamped to 1–12
Create a ticket for an event. Checks capacity. Applies 20% student discount.
Body:
{
"eventId": "clx...",
"name": "Mario Rossi",
"email": "mario@example.com",
"type": "student"
}Ticket types: general | student (20% off) | group
Response: { ticket: { id, qrCode, price, status } }
Create a Stripe Checkout session for a paid ticket.
Body:
{
"eventId": "clx...",
"eventTitle": "Concerto Jazz",
"name": "Mario Rossi",
"email": "mario@example.com",
"type": "general",
"price": 15
}Response: { sessionId: "cs_...", url: "https://checkout.stripe.com/..." }
Get tickets for the current user. Requires session.
Get threaded comments for an event.
Add a comment or reply. Requires session.
Body:
{
"eventId": "clx...",
"text": "Evento fantastico!",
"parentId": "clx..."
}Comments support 1 level of threading via parentId.
Generate or retrieve the user's calendar feed token. Requires session.
iCal feed of user's "going" events. No auth required (token-based).
Content-Type: text/calendar
Download a single event as .ics file. Returns Google Calendar and Outlook URLs.
List all venues with event counts.
| Parameter | Type | Description |
|---|---|---|
city |
string | Filter by city |
Create a new venue. Validates coordinates.
Claim a venue (sets status to pending). Requires session.
Find venues matching criteria (category, capacity, city). Scored by analytics.
Extract and normalize venues from existing events (admin operation).
Get venue weekly availability matrix.
Update venue availability. Requires venue ownership.
All AI endpoints gracefully fall back to heuristic methods when OPENAI_API_KEY is not set.
Classify an event into a category using AI.
Body: { title: string, description: string }
Response: { category: EventCategory, confidence: number }
AI-enhance an event description (Italian, engaging, with emoji).
Body: { title: string, description: string, venue: string }
Response: { enhanced: string, tokens: number }
Natural language event search (Italian/English).
Body: { query: string }
Response: { events: EventRecord[], interpretation: string }
Translate event content between Italian and English.
Body: { text: string, targetLang: "en" | "it" }
Response: { translated: string }
Conversational AI assistant for event discovery.
Track a funnel event (impression, detail_view, rsvp_intent, ticket_start, payment_complete, check_in).
Body:
{
"eventId": "clx...",
"sessionId": "browser-session-id",
"action": "detail_view",
"channel": "whatsapp"
}Get conversion funnel data for an event (counts, drop-off rates, conversion rates per stage).
Get aggregate analytics for the current organizer's events. Requires session.
Get notifications for the current user. Requires session.
| Parameter | Type | Default | Description |
|---|---|---|---|
unread |
boolean | false | Only unread notifications |
Mark notification(s) as read.
Body: { id: string } or { markAllRead: true }
Get notification preferences for the current user.
Update a notification preference.
Body:
{
"channel": "email",
"category": "events",
"enabled": false,
"frequency": "weekly"
}Channels: email | push | in_app
Categories: events | tickets | social | digest | reminders
Follow or unfollow a target. Requires session.
Body:
{
"targetType": "organizer",
"targetId": "clx...",
"action": "follow"
}Target types: user | organizer | venue
Get social feed based on followed users/organizers/venues.
Ingest a social signal from Telegram/WhatsApp for AI event extraction.
Join or leave an event waitlist. Requires session.
Body: { eventId: string, action: "join" | "leave" | "claim" }
Get waitlist position and depth for an event.
Create an event request (what the community wants).
Body:
{
"category": "musica",
"title": "Jazz Festival estivo",
"description": "Un weekend di jazz...",
"pledgeTarget": 500
}List open event requests, sorted by pledges and upvotes.
Pledge money toward an event request. Requires session.
Body: { requestId: string, amount: number }
Start Stripe Connect onboarding. Returns onboarding URL. Requires session (organizer role).
Get Stripe Connect status for the current organizer.
Response:
{
"accountId": "acct_...",
"onboarded": true,
"commissionRate": 0.03,
"tier": "verified",
"isLive": true
}Get payout history for the current organizer.
Get commission rate breakdown and platform revenue (admin only).
Get onboarding progress for the current user.
Advance onboarding step.
Set user interest categories during onboarding.
Submit a verification request with documentation.
Body:
{
"documentType": "partita_iva",
"documentUrl": "https://...",
"tier": "verified"
}Document types: partita_iva | association | municipal_letter
Tiers: verified (3% commission) | municipal_partner (2%) | featured_partner (2%)
Get verification status for the current user or list pending verifications (admin).
All admin endpoints require X-API-Key header matching ADMIN_API_KEY.
Get job queue statistics and recent jobs.
Process next pending job or enqueue a new job.
List all users with roles and verification tiers.
Health check endpoint. Returns service status and dependency checks.
Response:
{
"status": "healthy",
"checks": {
"database": { "ok": true },
"stripe": { "ok": true },
"email": { "ok": false, "message": "Not configured" },
"baseUrl": { "ok": true }
},
"timestamp": "2025-07-15T10:00:00.000Z"
}Status codes: 200 = healthy, 503 = degraded
Hourly cron endpoint (called by Vercel Cron). Authenticated via CRON_SECRET.
Runs: scraper pipeline, job queue processing, recurring job scheduling, alert checks.
Trigger a manual scraper run. Returns ingestion results per source.
Get/set user locale preference.
Upload a file (image). Returns URL.
Report an event for moderation.
Body: { eventId: string, category: "spam" | "misleading" | "inappropriate" | "other", reason?: string }
Auto-escalation: events with 3+ pending reports are auto-hidden.
Get personalized event recommendations for the current user (affinity-based with category diversity).
Get current user profile.
Update current user profile (name, bio, interests).
Get organizer dashboard data (events, tickets, analytics).
Preview weekly digest content.
Trigger digest email send.
List all enabled cities.
Generate Open Graph image for an event (dynamic).
Returns an embeddable HTML page with event cards.
| Parameter | Type | Default | Description |
|---|---|---|---|
city |
string | Forlì | City filter |
limit |
number | 5 | Number of events |
theme |
string | light | light or dark |
category |
string | — | Category filter |
CORS enabled. X-Frame-Options: ALLOWALL for this endpoint.
Dynamic OG image generation for social sharing.
Stripe webhook handler. Validates signature via STRIPE_WEBHOOK_SECRET.
Handled events:
checkout.session.completed→ Activates ticket, emitsticket.paiddomain eventcheckout.session.expired→ Cancels ticketaccount.updated→ Updates Connect onboarding statustransfer.created→ Marks payout as completedpayout.paid/payout.failed→ Logs for reconciliation
| Tier | Window | Max Requests | Applied To |
|---|---|---|---|
general |
1 minute | 100 | Most endpoints |
auth |
1 minute | 10 | Login/register |
payment |
1 minute | 5 | Checkout/payment |
upload |
1 minute | 10 | File uploads |
search |
1 minute | 30 | Search endpoints |
admin |
1 minute | 50 | Admin endpoints |
api |
1 hour | 1000 | Public API |
Rate limit headers are included in responses:
X-RateLimit-Remaining— Requests remaining in windowX-RateLimit-Reset— Window reset timestamp (ISO 8601)
When rate limited, the API returns 429 Too Many Requests:
{ "error": "Troppe richieste. Riprova tra poco." }All errors follow this shape:
{
"error": "Human-readable error message (Italian)"
}Common status codes:
400— Validation error or bad request401— Authentication required (missing session or API key)403— Insufficient permissions404— Resource not found429— Rate limited500— Internal server error