1. What the APIs allow
With an API key you can, from your software, read and write to the calendar of your venues:
- read venues and courts with their identifiers, which are required for all other calls;
- read bookings for a period, a venue or a single court, to align them with your database;
- create bookings externally (for example when a customer books from your website), with the same overlap and panel closure checks;
- modify or cancel an existing booking (change of time or court, payment collection, notes, cancellation);
- read the availability of a court on a given day: slots already booked, opening hours and extraordinary closures, so you can offer free slots without replicating the rules;
- read the price list of a court to show prices to the end customer.
Each key is linked to the manager account that created it and sees only the facilities, courts and bookings of that account. A resource belonging to another manager does not return 403 but 404 not_found: its existence is not revealed.
Organisers' requests for court use in tournaments (approved from the app), the creation of facilities and courts, and the saving of timetables, closures and price lists are not part of these APIs: these are operations of the manager panel.
2. Requirements and activation
- A VolleyFriends court manager account with at least one facility and one court.
- An active subscription with a plan that includes the 'API for external management systems' feature. The entire
v1— even read-only requests — is reserved for those who have this feature: it is a paid feature, not an add-on of the panel. - An API key generated from the management panel: admin.volleyfriends.com → Settings → API Keys → New key.
The key has the format vfk_ followed by 40 hexadecimal characters (44 characters in total) and is shown only once, immediately after creation: only its sha256 hash remains in the database, so it cannot be recovered in any way. If you lose it, revoke it and generate a new one. In the panel list you only see the prefix (e.g. vfk_9f2c), the creation date and the last used date.
3. Authentication
Each request must be authenticated with the X-Api-Key header. No tokens, logins, expirations or renewals are needed: the key remains valid until you revoke it.
# The keys on this page are examples: replace with your own.
curl -s https://www.volleyfriends.com/api/manager-api/v1/venues -H "X-Api-Key: vfk_9f2c41a7b83d05e6c19af4728b60d35e1c9a7f42"
In the following examples, the key is read from the VF_API_KEY environment variable, as should also be done in production.
Header missing, shorter than 12 characters, unknown or belonging to a revoked key: the response is 401 with this body.
{
"error": "invalid_key",
"message": "Chiave API mancante o non valida (header X-Api-Key)."
}
- The key acts as a password with full powers over the calendar of your facilities: do not insert it into pages, apps or browser-side JavaScript, where it would be readable by anyone. Use it only server-to-server.
- Do not put it in the source code or in a repository: use an environment variable or a secrets manager.
- Use one key for each connected system (website, management system, sync script): this way you can revoke one without shutting down the others.
- If you suspect it has been exposed, revoke it immediately from the panel: revocation is immediate and is never blocked by the subscription status. From that moment on, every call with that key receives
401 invalid_key. - Traffic exclusively over HTTPS; the 'last used' field in the panel helps you notice unexpected uses.
4. Base URL
https://www.volleyfriends.com/api
The paths documented below must be appended to the base. The /api prefix belongs to the reverse proxy, so the full address of an endpoint is, for example:
GET https://www.volleyfriends.com/api/manager-api/v1/availability
All responses are application/json with UTF-8 encoding. Requests with a body (POST, PATCH) must have Content-Type: application/json.
5. Rate limiting
The limit is 120 requests per minute per API key (1-minute sliding window). The count is per key, not per IP address: management systems are often behind the same provider address and an IP-based limit would penalise different managers hosted on the same infrastructure. If the X-Api-Key header is missing entirely, the count falls back to the IP address.
Every response reports the status of the counter in the x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset headers (seconds to reset). When exceeded, retry-after is also added.
Response when exceeded — 429
{
"error": "rate_limited",
"message": "Limite di 120 richieste al minuto superato per questa chiave API."
}
- For periodic alignment, use a single call to
/bookingswith a widefrom–tointerval, instead of one call per day. - Cache the list of venues and courts: it changes rarely.
- In case of
429, wait and retry with exponential backoff (for example 2, 4, 8 seconds), respectingretry-after.
6. Endpoints
Six endpoints, all under /manager-api/v1. Every call goes through the same chain of checks, in order: valid API key → "API for external management systems" feature included in the plan → rate limit → parameter validation → verification that the resource is yours.
6.1 Venues and courts
/api/manager-api/v1/venuesLists your venues, each with its own nested courts. This is the starting call: the court ids (courtId) are needed for all other endpoints. Venues are ordered by name, courts by name.
Parameters
None.
Example request
curl -s https://www.volleyfriends.com/api/manager-api/v1/venues -H "X-Api-Key: $VF_API_KEY"
Example response — 200
{
"venues": [
{
"id": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
"name": "Centro Sportivo Aurora",
"city": "Verona",
"address": "Via delle Palestre 12",
"verified": true,
"courts": [
{
"id": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
"venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
"name": "Campo 1",
"surface": "parquet",
"indoor": true,
"lighting": true
},
{
"id": "e2a71f64-9c03-4bd8-b512-38f6cd90a7e1",
"venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
"name": "Campo 2 beach",
"surface": "sabbia",
"indoor": false,
"lighting": true
}
]
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
venues[].id | uuid | Venue identifier (venueId in the other endpoints). |
venues[].name | string | Name of the venue. |
venues[].city | string | null | City. |
venues[].address | string | null | Address. |
venues[].verified | boolean | true if the venue has the VERIFIED badge. |
venues[].courts[] | array | Venue courts. Empty array if none. |
courts[].id | uuid | Court identifier (courtId in other endpoints). |
courts[].venueId | uuid | Venue the court belongs to. |
courts[].name | string | Court name. |
courts[].surface | string | null | Surface, free text entered by the manager. |
courts[].indoor | boolean | Indoor court. |
courts[].lighting | boolean | Lighting available. |
If the account has no venues yet, the response is {"venues": []}.
Specific errors
None other than the general ones.
6.2 Bookings list
/api/manager-api/v1/bookingsReturns the bookings of your venues, sorted by ascending start time. The list is paginated: maximum limit rows per call (default 500, cap 1000) — if you receive exactly limit rows, call again with an increased offset for the next page. In production, always filter by date range at least.
Parameters (query string)
| Name | Type | Req. | Description |
|---|---|---|---|
venueId | uuid | no | Limit to a single venue. If it is not yours: 404 not_found. |
courtId | uuid | no | Limit to a court. If it is not yours: 404 not_found. Combined with a venueId of another of your facilities, the result is an empty list. |
from | ISO date or timestamp | no | Lower bound inclusive. A plain date AAAA-MM-GG is equivalent to local Italian midnight on that day. |
to | ISO date or timestamp | no | Upper bound exclusive. A plain date is equivalent to local midnight of the following day: to=2026-08-31 includes all of 31 August. |
limit | integer 1–1000 | no | Maximum number of rows (default 500). |
offset | integer ≥ 0 | no | Rows to skip (default 0): offset=500 is the second page with the default limit. |
from and to apply to the booking's start time. A booking that started before from and is still ongoing after will not appear: if needed, extend the lower bound by a few hours. The filter does not exclude cancelled bookings: the list also contains those with status: "cancelled".
Example request
curl -s -G https://www.volleyfriends.com/api/manager-api/v1/bookings -H "X-Api-Key: $VF_API_KEY" --data-urlencode "courtId=7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05" --data-urlencode "from=2026-08-01" --data-urlencode "to=2026-08-31"
Example response — 200
{
"bookings": [
{
"id": "9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f",
"venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
"courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
"courtName": "Campo 1",
"startsAt": "2026-08-12T16:00:00.000Z",
"endsAt": "2026-08-12T17:30:00.000Z",
"customerName": "Marta Bellini",
"customerPhone": "+39 340 1122334",
"customerEmail": "marta.bellini@example.com",
"priceCents": 3000,
"playersCount": null,
"status": "confirmed",
"paymentMethod": null,
"paymentLink": null,
"notes": "Prenotato dal sito della struttura",
"source": "api",
"recurrenceId": null,
"createdAt": "2026-07-28T09:14:02.517Z",
"updatedAt": "2026-07-28T09:14:02.517Z"
}
],
"limit": 500,
"offset": 0
}
Booking fields
The same structure is returned by POST and PATCH (inside the booking key).
| Field | Type | Description |
|---|---|---|
id | uuid | Booking identifier. |
venueId | uuid | Venue. |
courtId | uuid | Booked court. |
courtName | string | Court name, for display convenience. |
startsAt | ISO timestamp | Start, always in UTC (suffix Z). |
endsAt | ISO timestamp | End, exclusive bound. |
customerName | string | Customer name. |
customerPhone | string | null | Phone. |
customerEmail | string | null | Email. |
priceCents | integer | null | Agreed price in cents. null = to be defined (never 0 for "undefined"). |
playersCount | integer | null | Declared number of people (only bookings on slots with price per person). null = not applicable or not specified. |
status | string | pending · confirmed · paid · cancelled — see statuses. |
paymentMethod | string | null | cash · bank_transfer · paypal · stripe · other. null = not collected. |
paymentLink | string | null | Card payment link generated by the panel, if present. Read-only. |
notes | string | null | Free-text notes. |
source | string | Source: manual (panel) · api (this API) · app (player request from the app) · tournament (tournament). |
recurrenceId | uuid | null | Present and identical on all occurrences of a recurring series created from the panel. |
createdAt | ISO timestamp | Creation. |
updatedAt | ISO timestamp | Last modified. |
Specific errors
| Code | error | When |
|---|---|---|
| 400 | invalid_input | venueId/courtId are not UUIDs, or from/to are not parsable dates. |
| 404 | not_found | The venue ("Struttura non trovata") or the court ("Campo non trovato") does not exist or does not belong to you. |
6.3 New booking
/api/manager-api/v1/bookingsCreates a booking on one of your courts. The booking is created with source: "api", so it can be distinguished in the panel from those entered manually, and the manager receives a "New booking" push notification on the app.
The same rules as the panel are applied, in order:
- extraordinary closure of the venue on the start date →
409 venue_closed; - overlap with another non-cancelled booking on the same court →
409 overlap; - price missing → calculated from the court's price list (
nullif the start time does not fall within any slot). If the slot has a price per person,playersCountis also required: without it, the request is rejected with400 players_required(unless an explicitpriceCentsis specified).
The opening hours do not block creation: they are informative, the manager remains in full control of their court. If you want to prevent out-of-hours bookings, check the hours returned by /availability before calling the POST.
Request body (JSON)
| Name | Type | Req. | Description |
|---|---|---|---|
courtId | uuid | yes | Court to be booked. It must belong to one of your facilities. |
startsAt | ISO 8601 instant with offset | yes | Start. Both the 2026-08-12T18:00:00Z and 2026-08-12T18:00:00+02:00 formats are accepted. Without offset: 400. |
endsAt | ISO 8601 instant with offset | yes | End, must be after startsAt. |
customerName | string (1–160) | yes | Customer name. |
customerPhone | string (max 40) | no | Phone. |
customerEmail | email (max 200) | no | Email, validated as an address. |
priceCents | integer ≥ 0 | no | Price in cents. If omitted, it is calculated from the court's price list. Enter 0 only for a truly free booking. |
playersCount | integer 1–500 | no | Number of people. Required if the price is to be calculated from the price list and the start time slot is per person; the total applies any guaranteed minimum for the slot. |
notes | string (max 2000) | no | Free-text notes, visible to the manager in the dashboard. |
status | pending | confirmed | paid | cancelled | no | Default confirmed. From an external management system, use confirmed or paid: pending is reserved for requests coming from the players' app (see statuses). |
paymentMethod | cash | bank_transfer | paypal | stripe | other | no | How it was (or will be) collected. Omitted = not collected. |
recurrence | object | no | Not supported by this API: if present, the request is rejected with 400 recurrence_unsupported (no booking created). Recurring series remain a dashboard feature: from an external management system, repeat the POST for each occurrence. |
Example request
curl -s -X POST https://www.volleyfriends.com/api/manager-api/v1/bookings -H "X-Api-Key: $VF_API_KEY" -H "Content-Type: application/json" -d '{
"courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
"startsAt": "2026-08-12T18:00:00+02:00",
"endsAt": "2026-08-12T19:30:00+02:00",
"customerName": "Marta Bellini",
"customerPhone": "+39 340 1122334",
"customerEmail": "marta.bellini@example.com",
"notes": "Prenotato dal sito della struttura"
}'
Response example — 201
{
"booking": {
"id": "9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f",
"venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
"courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
"courtName": "Campo 1",
"startsAt": "2026-08-12T16:00:00.000Z",
"endsAt": "2026-08-12T17:30:00.000Z",
"customerName": "Marta Bellini",
"customerPhone": "+39 340 1122334",
"customerEmail": "marta.bellini@example.com",
"priceCents": 3000,
"playersCount": null,
"status": "confirmed",
"paymentMethod": null,
"paymentLink": null,
"notes": "Prenotato dal sito della struttura",
"source": "api",
"recurrenceId": null,
"createdAt": "2026-07-28T09:14:02.517Z",
"updatedAt": "2026-07-28T09:14:02.517Z"
}
}
In the example, priceCents was not specified: 90 minutes in a 20,00 €/hour slot results in 3000 cents (the calculation is proportional to the actual minutes, using the rate of the slot in which the booking starts).
Specific errors
| Code | error | When |
|---|---|---|
| 400 | invalid_input | Invalid body: missing required field, instant without offset, endsAt not after startsAt, invalid email, lengths out of bounds. |
| 400 | players_required | Price to be calculated from the price list on a per person slot, but playersCount is missing. |
| 400 | recurrence_unsupported | The body contains recurrence: recurrences are not available from this API. |
| 403 | subscription_required | No active manager subscription. |
| 404 | not_found | "Campo non trovato" — the courtId does not exist or is not yours. |
| 409 | venue_closed | The venue has an extraordinary closure on the start date. |
| 409 | overlap | The court already has a non-cancelled booking overlapping the interval. |
// 409 — overlap
{ "error": "overlap", "message": "Il campo è già prenotato in quell'orario." }
// 409 — extraordinary closure (the reason, if specified by the manager, is in the message)
{ "error": "venue_closed", "message": "Struttura chiusa in quella data (manutenzione)." }
There is no idempotency key: repeating the same POST after a timeout can create a duplicate or, more often, return 409 overlap because the first attempt was successful. In case of an uncertain response, before retrying verify with GET /bookings on the affected interval.
6.4 Update booking
/api/manager-api/v1/bookings/{id}Updates an existing booking. It is a partial update: send only the fields that change, the others remain unchanged. The body must contain at least one field.
There is no deletion endpoint: to cancel a booking, set status: "cancelled". The history remains in the panel and the slot becomes free again (cancelled bookings do not occupy the court and do not generate overlap).
Path parameter
| Name | Type | Req. | Description |
|---|---|---|---|
id | uuid | yes | Booking ID, from GET /bookings or from the POST response. |
Request body (JSON)
All fields are optional. Those marked "nullable" accept null to clear the value.
| Name | Type | Notes |
|---|---|---|
courtId | uuid | Moves the booking to another of your courts, even in another of your venues (venueId is updated accordingly). |
startsAt | ISO instant with offset | New start. |
endsAt | ISO instant with offset | New end. The final result must remain after the start, even when combining only one end with the already saved value. |
customerName | string (1–160) | — |
customerPhone | string (max 40) | Nullable. |
customerEmail | email (max 200) | Nullable. |
priceCents | integer ≥ 0 | Nullable (null = price to be defined). In PATCH, the price is not recalculated from the price list: if you move the time and want the new price, specify it. |
playersCount | integer 1–500 | Nullable. Updates only the data: the price is not automatically recalculated — if the number of people changes the total, send the new priceCents as well. |
notes | string (max 2000) | Nullable. |
status | pending | confirmed | paid | cancelled | Any transition is allowed. This is also the way to respond to a request received from the app (status: "pending" with source: "app"): change it to confirmed to accept it or to cancelled to reject it — the player receives the notification. |
paymentMethod | cash | bank_transfer | paypal | stripe | other | Nullable. |
The closure and overlap checks are performed again in two cases: when the request moves the slot (startsAt, endsAt or courtId) and when it reactivates a cancelled booking (status from cancelled to any other state) — as a cancelled booking it did not occupy the court, and in the meantime the slot may have been taken: in that case the PATCH responds with 409 overlap or 409 venue_closed. A PATCH that only changes other fields (price, notes, revenue) does not repeat them.
Example request — rescheduling and revenue
curl -s -X PATCH https://www.volleyfriends.com/api/manager-api/v1/bookings/9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f -H "X-Api-Key: $VF_API_KEY" -H "Content-Type: application/json" -d '{
"startsAt": "2026-08-12T19:00:00+02:00",
"endsAt": "2026-08-12T20:30:00+02:00",
"status": "paid",
"paymentMethod": "cash",
"priceCents": 3000
}'
Example request — cancellation
curl -s -X PATCH https://www.volleyfriends.com/api/manager-api/v1/bookings/9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f -H "X-Api-Key: $VF_API_KEY" -H "Content-Type: application/json" -d '{ "status": "cancelled" }'
Example response — 200
The updated booking, in the same format as the POST:
{
"booking": {
"id": "9d51c7b4-0e28-4f3a-a6d9-72b48e105c3f",
"venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
"courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
"courtName": "Campo 1",
"startsAt": "2026-08-12T17:00:00.000Z",
"endsAt": "2026-08-12T18:30:00.000Z",
"customerName": "Marta Bellini",
"customerPhone": "+39 340 1122334",
"customerEmail": "marta.bellini@example.com",
"priceCents": 3000,
"playersCount": null,
"status": "paid",
"paymentMethod": "cash",
"paymentLink": null,
"notes": "Prenotato dal sito della struttura",
"source": "api",
"recurrenceId": null,
"createdAt": "2026-07-28T09:14:02.517Z",
"updatedAt": "2026-07-28T11:02:47.883Z"
}
}
Specific errors
| Code | error | When |
|---|---|---|
| 400 | invalid_input | Empty body ("Nessun campo da aggiornare"), invalid values, or end time not after start time ("L'orario di fine deve essere successivo a quello di inizio"). |
| 403 | subscription_required | No active manager subscription. |
| 404 | not_found | "Prenotazione non trovata" or "Campo non trovato" if the new courtId is not yours. |
| 409 | venue_closed | Rescheduling to an extraordinary closure date. |
| 409 | overlap | The new time overlaps with another non-cancelled booking on the same court. |
6.5 Court availability on a day
/api/manager-api/v1/availabilityReturns, for a court and a day, the occupied slots, the opening hours for that day of the week, and any extraordinary closure. This is the endpoint to use to calculate free slots on an external site without duplicating the management system's rules: subtract bookings from the opening window.
All slots occupying the court are listed, in any status except cancelled: therefore including online booking requests that are still pending. The day is the local Italian day and the intervals are half-open: a booking ending exactly at midnight belongs to the previous day. A booking spanning midnight appears in both days it intersects, with its actual times (which may fall outside the requested day).
Parameters (query string)
| Name | Type | Req. | Description |
|---|---|---|---|
courtId | uuid | yes | Court whose availability is to be read. It must be yours. |
date | AAAA-MM-GG | yes | Local Italian day. Different format: 400 with message "Data non valida (AAAA-MM-GG)". |
Example request
curl -s -G https://www.volleyfriends.com/api/manager-api/v1/availability -H "X-Api-Key: $VF_API_KEY" --data-urlencode "courtId=7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05" --data-urlencode "date=2026-08-12"
Example response — 200
{
"courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
"venueId": "b3f8e5c1-2d47-4a90-8f31-6c0d9a7e4512",
"date": "2026-08-12",
"timezone": "Europe/Rome",
"hours": {
"weekday": 2,
"openMinute": 540,
"closeMinute": 1380,
"closed": false,
"configured": true
},
"closure": null,
"bookings": [
{
"startsAt": "2026-08-12T16:00:00.000Z",
"endsAt": "2026-08-12T17:30:00.000Z",
"status": "confirmed"
},
{
"startsAt": "2026-08-12T18:00:00.000Z",
"endsAt": "2026-08-12T19:00:00.000Z",
"status": "paid"
},
{
"startsAt": "2026-08-12T19:00:00.000Z",
"endsAt": "2026-08-12T20:30:00.000Z",
"status": "pending"
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
courtId | uuid | Required field. |
venueId | uuid | Court facility. |
date | AAAA-MM-GG | Requested day, repeated. |
timezone | string | Always "Europe/Rome": the timezone in which date and the time minutes must be interpreted. |
hours.weekday | integer 0–6 | Day of the week: 0 = Monday … 6 = Sunday. |
hours.openMinute | integer | null | Opening in minutes from local midnight (540 = 09:00). null if the day is declared closed. |
hours.closeMinute | integer | null | Closing in minutes (1380 = 23:00). null if the day is declared closed. |
hours.closed | boolean | true if the manager has declared that day of the week as closed. |
hours.configured | boolean | false when the manager has not set the hours for that day: in that case openMinute/closeMinute take the default convention 480 (08:00) – 1380 (23:00), which is a display fallback, not a declared time. |
closure | object | null | Extraordinary closure active on the requested day, or null. |
closure.from | AAAA-MM-GG | First day of closure. |
closure.to | AAAA-MM-GG | Last day of closure, inclusive. |
closure.reason | string | null | Reason, if specified by the manager. |
bookings[] | array | Booked slots, ordered by start. Only startsAt, endsAt and status (pending, confirmed or paid): no customer data, so the response can be used to build a public calendar. |
When closure is not null, the day is to be considered non-bookable, regardless of hours: a POST on that date would receive 409 venue_closed.
Specific errors
| Code | error | When |
|---|---|---|
| 400 | invalid_input | courtId missing or not UUID, date missing or not in AAAA-MM-GG format. |
| 404 | not_found | "Campo non trovato" — the court does not exist or is not yours. |
6.6 Court rates
/api/manager-api/v1/ratesThe court rates in read-only mode: used to show prices to the customer before booking, with the same time slots that the management system uses to calculate the automatic price. Time slots can only be modified from the dashboard. They are sorted by ascending fromMinute.
Parameters (query string)
| Name | Type | Req. | Description |
|---|---|---|---|
courtId | uuid | yes | Court whose rates are to be read. It must be yours. |
Example request
curl -s -G https://www.volleyfriends.com/api/manager-api/v1/rates -H "X-Api-Key: $VF_API_KEY" --data-urlencode "courtId=7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05"
Example response — 200
{
"courtId": "7c94a0d2-58b1-4e6f-9a3c-1d20f8b47e05",
"rates": [
{
"id": "4a1e0b72-6d35-4c81-93af-0b7e2d5814c6",
"label": "Mattina infrasettimanale",
"daysMask": 31,
"fromMinute": 480,
"toMinute": 840,
"pricePerHourCents": 1500,
"pricingMode": "court",
"minPlayers": null
},
{
"id": "c07d9b31-8f42-4a05-b6e9-15d3c8017a2b",
"label": "Serale",
"daysMask": 127,
"fromMinute": 1080,
"toMinute": 1380,
"pricePerHourCents": 600,
"pricingMode": "person",
"minPlayers": 10
}
]
}
Response fields
| Field | Type | Description |
|---|---|---|
courtId | uuid | Required field. |
rates[].id | uuid | Identifier of the time slot. |
rates[].label | string | Label chosen by the manager (e.g. 'Evening'). |
rates[].daysMask | integer 1–127 | Bitmask of days: Mon 1, Tue 2, Wed 4, Thu 8, Fri 16, Sat 32, Sun 64. 127 = every day, 31 = Monday to Friday. |
rates[].fromMinute | integer 0–1440 | Start of the time slot in minutes from local midnight. |
rates[].toMinute | integer 0–1440 | End of the time slot, always greater than fromMinute. |
rates[].pricePerHourCents | integer > 0 | Hourly price in cents (2000 = 20,00 €). With pricingMode: "person" it is the hourly price per person (600 = 6,00 €/person per hour). |
rates[].pricingMode | court | person | court = court price (default). person = price per person: the total scales with the number of people, never downwards. |
rates[].minPlayers | integer | null | Guaranteed minimum for the tier per person: below that number, you still pay for the minimum. null = no minimum. |
We look at the start time: among the court's time slots, we take the first one (ordered by fromMinute) whose day is active in the bitmask and which contains the start minute (fromMinute ≤ inizio < toMinute). The price is proportional to the actual minutes: arrotonda(pricePerHourCents × minuti / 60) — 90 minutes at 20,00 €/hour makes 30,00 €. A booking spanning two slots is priced at the rate of the start slot. No matching slot: priceCents is null (price to be defined), never 0.
With a per person tier (pricingMode: "person"), the hourly result is multiplied by the number of billed people: max(playersCount, minPlayers). Example with the evening tier above (6,00 €/person per hour, minimum 10): 60 minutes for 12 people → 72,00 €; for 16 → 96,00 € (the price per person does not decrease); for 8 → 60,00 € (the guaranteed minimum is triggered). Without playersCount, the price cannot be calculated: null when reading, 400 players_required in the POST that relies on the price list.
Specific errors
| Code | error | When |
|---|---|---|
| 400 | invalid_input | courtId missing or not UUID. |
| 404 | not_found | "Campo non trovato" — the court does not exist or is not yours. |
A court without a price list returns 200 with "rates": [].
7. Data conventions
7.1 Dates and times
- Inbound timestamps are ISO 8601 strings with a mandatory offset:
2026-08-12T18:00:00+02:00or2026-08-12T18:00:00Z. A string without an offset (2026-08-12T18:00:00) is rejected with400: it would be ambiguous, and for a calendar, this ambiguity results in bookings being off by an hour. - Outbound timestamps are always normalized to UTC with a
Zsuffix and milliseconds:"2026-08-12T16:00:00.000Z". Convert them to the local time zone before displaying them. - Plain dates (
date,closure.from,closure.to, andfrom/towhen used in day format) areAAAA-MM-GGand refer to the local Italian day. - Intervals are half-open
[inizio, fine): 18:00–19:00 and 19:00–20:00 are not in conflict.
7.2 Time zone
Bookings are stored as absolute timestamps, but all calendar rules — day of the week, price list tiers, opening hours, closing dates, day boundaries — are evaluated in the Europe/Rome time zone, with daylight saving time handled automatically. The timezone field in the /availability response explicitly declares this. If your server runs in UTC or another time zone, do not manually convert the minutes: they are already expressed in local Italian time.
7.3 Minutes from midnight
"Calendar" times (openMinute, closeMinute, fromMinute, toMinute) are integers from 0 to 1440 counting the minutes from local midnight. Conversions: ore = ⌊m / 60⌋, minuti = m mod 60.
| Minutes | Local time | Minutes | Local time |
|---|---|---|---|
| 0 | 00:00 | 1080 | 18:00 |
| 480 | 08:00 | 1320 | 22:00 |
| 540 | 09:00 | 1380 | 23:00 |
| 840 | 14:00 | 1440 | 24:00 (end of day) |
7.4 Amounts
All amounts are integers in euro cents (priceCents, pricePerHourCents): no floating-point values, no currency symbols. 2000 = 20,00 €. On a booking, priceCents: null means "price to be defined" and is different from 0, which means "free of charge".
7.5 Booking statuses
| Status | Meaning | Does it occupy the slot? |
|---|---|---|
pending | Online booking request pending the manager's decision. It only originates from requests sent by players from the VolleyFriends app (source: "app"): the manager accepts (confirmed) or rejects (cancelled) it. | Yes — the slot is already blocked, to avoid double requests for the same time |
confirmed | Valid booking, not yet paid. It is the initial state of every booking created by these APIs and the panel. | Yes |
paid | Valid and paid booking. Normally accompanied by paymentMethod. | Yes |
cancelled | Booking cancelled (or request rejected). It remains in the history but frees up the court: it does not generate overlap and does not appear in /availability. | No |
- You can find them in GET /bookings and in the occupied slots of /availability: treat the slot as unavailable, exactly like a
confirmedone. - You can reply to the request from your management system with a PATCH:
{"status": "confirmed"}to accept it,{"status": "cancelled"}to reject it. The player receives a notification with the outcome. - Identify them by the pair
status: "pending"+source: "app". The customer's details come from the player's VolleyFriends profile (name and email), socustomerPhonecan benull. - Do not create
pendingbookings from these APIs: the value is accepted by validation, but that state describes a request awaiting a response from the manager and no one would decide on it on your end. If there is a cart or a payment to be completed in your flow, keep the wait in your system and call the POST when the booking is final — or create it asconfirmedand change it tocancelledif the payment does not arrive.
7.6 Source and payment method
| Field | Values | Meaning |
|---|---|---|
source | manual | Entered by the manager in the panel. |
api | Created by an external management system using these APIs. Value always set by the POST, non-modifiable. | |
app | Online booking request sent by a player from the VolleyFriends app: it starts as pending and awaits your decision. | |
tournament | Generated by the organisation of a tournament on the facility's courts. | |
paymentMethod | cash, bank_transfer, paypal, stripe, other | How it was collected. null = not collected. |
Both are text domains: new values may be added in the future without changing the version, so your code must not fail on an unknown value.
7.7 Customers' personal data
Name, phone number and email that you send in the bookings are personal data of your customers: you are the data controller, VolleyFriends stores them on your behalf as a data processor pursuant to art. 28 GDPR. Only send what is necessary for court management and inform your customers. Details can be found in the privacy policy.
8. General errors
Every error has the same body: a stable code in error, designed for your code, and a message in Italian, designed for people. Base your decisions on error and the HTTP status, not on the message text, which may change.
{
"error": "overlap",
"message": "Il campo è già prenotato in quell'orario."
}
Validation errors add issues, the detailed list of problems field by field:
{
"error": "invalid_input",
"message": "Dati non validi",
"issues": [
{
"code": "invalid_string",
"validation": "datetime",
"path": ["startsAt"],
"message": "Invalid datetime"
}
]
}
| HTTP | error | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_input | Invalid parameters or body. The details are in issues. | Correct the request. Do not retry identically: the outcome will not change. |
| 401 | invalid_key | Header X-Api-Key missing, malformed, unknown or revoked. | Verify that you are sending the header and that the key is active in the panel. If it has been revoked, generate a new one. |
| 403 | subscription_required | No active "Court Manager" subscription on the account owning the key. | Reactivate the subscription from the app (Profile → manager area → Subscription). No calls will work until then. |
| 403 | feature_not_included | Active subscription, but the plan does not include "API for external management systems". The message names the current plan. | Upgrade to a plan that includes this feature. This applies to the entire v1, including reads. |
| 404 | not_found | Resource does not exist or belongs to another manager: the two situations are not distinguished, so as not to reveal other people's data. | Re-read the IDs from /venues and from /bookings: it is almost always an old ID or one copied from another account. |
| 409 | overlap | The court is already occupied in the requested interval by a booking that has not been cancelled. | Propose another slot: re-read /availability. Do not retry without changing the time. |
| 409 | venue_closed | Extraordinary closure of the facility on the requested date. | Propose another date. Closures can be read from /availability (closure). |
| 429 | rate_limited | Exceeded 120 requests per minute for the key. | Wait and retry with exponential backoff, respecting retry-after. |
| 4xx | error | Generic code for request errors that do not fall into the cases above (rare). | Read message and correct the request. |
| 500 | internal | Unexpected error on our side. | Try again later. If it persists, write to us indicating the time, endpoint and key prefix (not the key). |
A non-existent path under the base returns 404; the same applies to an unsupported method on an existing path:
{
"error": "not_found",
"message": "Risorsa non trovata"
}
The 502, 503 and 504 responses come from the reverse proxy when the API is unreachable (restart or maintenance): in those cases the body is not guaranteed to be in JSON format. Treat them as temporary errors and retry with backoff, without attempting to read their content.
9. Versions and compatibility
The version is in the path: /manager-api/v1. There are no version headers or release dates to specify: the only version currently published is v1.
What we can change without notifying you
- Adding fields to existing responses.
- Adding optional parameters to requests, with a default value that preserves the current behaviour.
- Adding new endpoints under
/v1. - Adding values to open text fields (
source,paymentMethod,surface). - Reformulating the
messageof errors, keeping theerrorcodes unchanged.
What does not change within v1
- No fields will be removed or renamed from responses.
- No fields will change type or meaning.
- No parameters that are currently optional will become mandatory.
- The
errorcodes and HTTP statuses documented here remain the same.
Any change that breaks these guarantees will arrive on a new prefix (/v2), with v1 kept in service and advance notice given to the managers using it.
- Ignore fields you do not recognise instead of failing on them (no 'strict' parsing that rejects unexpected properties).
- Handle unknown values of open text fields with a fallback branch.
- Do not rely on the order of JSON keys or on the text of messages.
- Treat
nulland a missing field as equivalent when reading.
Changelog
| Version | Date | Notes |
|---|---|---|
| v1 | Jul 2026 | Compatible additions: per-person rates in the price list (pricingMode, minPlayers) and playersCount on bookings (with the players_required error); limit/offset pagination on the booking list; conflict re-checking when reactivating a cancelled booking; recurrence is now explicitly rejected with 400 recurrence_unsupported (previously it was silently ignored). |
| v1 | 2026 | First public version: venues and courts, bookings (list, creation, modification), daily availability, price list. Authentication with X-Api-Key, 120 requests/minute per key. |
10. Integration support
For technical questions, unexpected behaviour or requests for endpoints not found here, write to us from the Contact page. To get straight to the point, please include in your message:
- the endpoint and method you are calling;
- the date and time of the call (including timezone) and the HTTP code received;
- the
errorcode and response message; - the prefix of the key used (e.g.
vfk_9f2c).
Do not send us the full API key through any channel, not even to have us investigate an issue: the prefix is enough for us to identify it. If you have already shared it with someone, revoke it and generate a new one.
If you need to test the integration without affecting the live calendar, create a test venue with a dedicated court and use that: all calls are restricted to the venues of the key owner, so the rest of your account is not affected.