API for external management systems

Version v1 — documentation updated to 29 July 2026

This page documents the HTTP APIs that VolleyFriends provides to facility managers to keep the court calendar in sync with external management software or an online booking website. The same domain rules as the manager panel apply (conflicts, closures, price lists): only the authentication method changes.

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.
Scope of the keys

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

  1. A VolleyFriends court manager account with at least one facility and one court.
  2. An active subscription with a plan that includes the 'API for external management systems' feature. The entire v1even read-only requests — is reserved for those who have this feature: it is a paid feature, not an add-on of the panel.
  3. An API key generated from the management panel: admin.volleyfriends.comSettingsAPI KeysNew 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)."
}
Safekeeping of the 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."
}
How to stay within the limit
  • For periodic alignment, use a single call to /bookings with a wide fromto interval, 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), respecting retry-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

GET/api/manager-api/v1/venues

Lists 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

FieldTypeDescription
venues[].iduuidVenue identifier (venueId in the other endpoints).
venues[].namestringName of the venue.
venues[].citystring | nullCity.
venues[].addressstring | nullAddress.
venues[].verifiedbooleantrue if the venue has the VERIFIED badge.
venues[].courts[]arrayVenue courts. Empty array if none.
courts[].iduuidCourt identifier (courtId in other endpoints).
courts[].venueIduuidVenue the court belongs to.
courts[].namestringCourt name.
courts[].surfacestring | nullSurface, free text entered by the manager.
courts[].indoorbooleanIndoor court.
courts[].lightingbooleanLighting available.

If the account has no venues yet, the response is {"venues": []}.

Specific errors

None other than the general ones.

6.2 Bookings list

GET/api/manager-api/v1/bookings

Returns 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)

NameTypeReq.Description
venueIduuidnoLimit to a single venue. If it is not yours: 404 not_found.
courtIduuidnoLimit 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.
fromISO date or timestampnoLower bound inclusive. A plain date AAAA-MM-GG is equivalent to local Italian midnight on that day.
toISO date or timestampnoUpper bound exclusive. A plain date is equivalent to local midnight of the following day: to=2026-08-31 includes all of 31 August.
limitinteger 1–1000noMaximum number of rows (default 500).
offsetinteger ≥ 0noRows to skip (default 0): offset=500 is the second page with the default limit.
What the filter applies to

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).

FieldTypeDescription
iduuidBooking identifier.
venueIduuidVenue.
courtIduuidBooked court.
courtNamestringCourt name, for display convenience.
startsAtISO timestampStart, always in UTC (suffix Z).
endsAtISO timestampEnd, exclusive bound.
customerNamestringCustomer name.
customerPhonestring | nullPhone.
customerEmailstring | nullEmail.
priceCentsinteger | nullAgreed price in cents. null = to be defined (never 0 for "undefined").
playersCountinteger | nullDeclared number of people (only bookings on slots with price per person). null = not applicable or not specified.
statusstringpending · confirmed · paid · cancelled — see statuses.
paymentMethodstring | nullcash · bank_transfer · paypal · stripe · other. null = not collected.
paymentLinkstring | nullCard payment link generated by the panel, if present. Read-only.
notesstring | nullFree-text notes.
sourcestringSource: manual (panel) · api (this API) · app (player request from the app) · tournament (tournament).
recurrenceIduuid | nullPresent and identical on all occurrences of a recurring series created from the panel.
createdAtISO timestampCreation.
updatedAtISO timestampLast modified.

Specific errors

CodeerrorWhen
400invalid_inputvenueId/courtId are not UUIDs, or from/to are not parsable dates.
404not_foundThe venue ("Struttura non trovata") or the court ("Campo non trovato") does not exist or does not belong to you.

6.3 New booking

POST/api/manager-api/v1/bookings

Creates 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:

  1. extraordinary closure of the venue on the start date → 409 venue_closed;
  2. overlap with another non-cancelled booking on the same court → 409 overlap;
  3. price missing → calculated from the court's price list (null if the start time does not fall within any slot). If the slot has a price per person, playersCount is also required: without it, the request is rejected with 400 players_required (unless an explicit priceCents is 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)

NameTypeReq.Description
courtIduuidyesCourt to be booked. It must belong to one of your facilities.
startsAtISO 8601 instant with offsetyesStart. Both the 2026-08-12T18:00:00Z and 2026-08-12T18:00:00+02:00 formats are accepted. Without offset: 400.
endsAtISO 8601 instant with offsetyesEnd, must be after startsAt.
customerNamestring (1–160)yesCustomer name.
customerPhonestring (max 40)noPhone.
customerEmailemail (max 200)noEmail, validated as an address.
priceCentsinteger ≥ 0noPrice in cents. If omitted, it is calculated from the court's price list. Enter 0 only for a truly free booking.
playersCountinteger 1–500noNumber 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.
notesstring (max 2000)noFree-text notes, visible to the manager in the dashboard.
statuspending | confirmed | paid | cancellednoDefault confirmed. From an external management system, use confirmed or paid: pending is reserved for requests coming from the players' app (see statuses).
paymentMethodcash | bank_transfer | paypal | stripe | othernoHow it was (or will be) collected. Omitted = not collected.
recurrenceobjectnoNot 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

CodeerrorWhen
400invalid_inputInvalid body: missing required field, instant without offset, endsAt not after startsAt, invalid email, lengths out of bounds.
400players_requiredPrice to be calculated from the price list on a per person slot, but playersCount is missing.
400recurrence_unsupportedThe body contains recurrence: recurrences are not available from this API.
403subscription_requiredNo active manager subscription.
404not_found"Campo non trovato" — the courtId does not exist or is not yours.
409venue_closedThe venue has an extraordinary closure on the start date.
409overlapThe 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)." }
Idempotency

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

PATCH/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

NameTypeReq.Description
iduuidyesBooking 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.

NameTypeNotes
courtIduuidMoves the booking to another of your courts, even in another of your venues (venueId is updated accordingly).
startsAtISO instant with offsetNew start.
endsAtISO instant with offsetNew end. The final result must remain after the start, even when combining only one end with the already saved value.
customerNamestring (1–160)
customerPhonestring (max 40)Nullable.
customerEmailemail (max 200)Nullable.
priceCentsinteger ≥ 0Nullable (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.
playersCountinteger 1–500Nullable. Updates only the data: the price is not automatically recalculated — if the number of people changes the total, send the new priceCents as well.
notesstring (max 2000)Nullable.
statuspending | confirmed | paid | cancelledAny 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.
paymentMethodcash | bank_transfer | paypal | stripe | otherNullable.
When conflicts are rechecked

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

CodeerrorWhen
400invalid_inputEmpty 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").
403subscription_requiredNo active manager subscription.
404not_found"Prenotazione non trovata" or "Campo non trovato" if the new courtId is not yours.
409venue_closedRescheduling to an extraordinary closure date.
409overlapThe new time overlaps with another non-cancelled booking on the same court.

6.5 Court availability on a day

GET/api/manager-api/v1/availability

Returns, 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)

NameTypeReq.Description
courtIduuidyesCourt whose availability is to be read. It must be yours.
dateAAAA-MM-GGyesLocal 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

FieldTypeDescription
courtIduuidRequired field.
venueIduuidCourt facility.
dateAAAA-MM-GGRequested day, repeated.
timezonestringAlways "Europe/Rome": the timezone in which date and the time minutes must be interpreted.
hours.weekdayinteger 0–6Day of the week: 0 = Monday … 6 = Sunday.
hours.openMinuteinteger | nullOpening in minutes from local midnight (540 = 09:00). null if the day is declared closed.
hours.closeMinuteinteger | nullClosing in minutes (1380 = 23:00). null if the day is declared closed.
hours.closedbooleantrue if the manager has declared that day of the week as closed.
hours.configuredbooleanfalse 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.
closureobject | nullExtraordinary closure active on the requested day, or null.
closure.fromAAAA-MM-GGFirst day of closure.
closure.toAAAA-MM-GGLast day of closure, inclusive.
closure.reasonstring | nullReason, if specified by the manager.
bookings[]arrayBooked 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.
With a closure in progress

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

CodeerrorWhen
400invalid_inputcourtId missing or not UUID, date missing or not in AAAA-MM-GG format.
404not_found"Campo non trovato" — the court does not exist or is not yours.

6.6 Court rates

GET/api/manager-api/v1/rates

The 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)

NameTypeReq.Description
courtIduuidyesCourt 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

FieldTypeDescription
courtIduuidRequired field.
rates[].iduuidIdentifier of the time slot.
rates[].labelstringLabel chosen by the manager (e.g. 'Evening').
rates[].daysMaskinteger 1–127Bitmask of days: Mon 1, Tue 2, Wed 4, Thu 8, Fri 16, Sat 32, Sun 64. 127 = every day, 31 = Monday to Friday.
rates[].fromMinuteinteger 0–1440Start of the time slot in minutes from local midnight.
rates[].toMinuteinteger 0–1440End of the time slot, always greater than fromMinute.
rates[].pricePerHourCentsinteger > 0Hourly price in cents (2000 = 20,00 €). With pricingMode: "person" it is the hourly price per person (600 = 6,00 €/person per hour).
rates[].pricingModecourt | personcourt = court price (default). person = price per person: the total scales with the number of people, never downwards.
rates[].minPlayersinteger | nullGuaranteed minimum for the tier per person: below that number, you still pay for the minimum. null = no minimum.
How the price of a booking is calculated

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

CodeerrorWhen
400invalid_inputcourtId missing or not UUID.
404not_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:00 or 2026-08-12T18:00:00Z. A string without an offset (2026-08-12T18:00:00) is rejected with 400: 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 Z suffix 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, and from/to when used in day format) are AAAA-MM-GG and 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.

MinutesLocal timeMinutesLocal time
000:00108018:00
48008:00132022:00
54009:00138023:00
84014:00144024: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

StatusMeaningDoes it occupy the slot?
pendingOnline 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
confirmedValid booking, not yet paid. It is the initial state of every booking created by these APIs and the panel.Yes
paidValid and paid booking. Normally accompanied by paymentMethod.Yes
cancelledBooking 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
How to handle "pending" bookings
  • You can find them in GET /bookings and in the occupied slots of /availability: treat the slot as unavailable, exactly like a confirmed one.
  • 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), so customerPhone can be null.
  • Do not create pending bookings 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 as confirmed and change it to cancelled if the payment does not arrive.

7.6 Source and payment method

FieldValuesMeaning
sourcemanualEntered by the manager in the panel.
apiCreated by an external management system using these APIs. Value always set by the POST, non-modifiable.
appOnline booking request sent by a player from the VolleyFriends app: it starts as pending and awaits your decision.
tournamentGenerated by the organisation of a tournament on the facility's courts.
paymentMethodcash, bank_transfer, paypal, stripe, otherHow 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"
    }
  ]
}
HTTPerrorMeaningWhat to do
400invalid_inputInvalid parameters or body. The details are in issues.Correct the request. Do not retry identically: the outcome will not change.
401invalid_keyHeader 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.
403subscription_requiredNo 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.
403feature_not_includedActive 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.
404not_foundResource 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.
409overlapThe 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.
409venue_closedExtraordinary closure of the facility on the requested date.Propose another date. Closures can be read from /availability (closure).
429rate_limitedExceeded 120 requests per minute for the key.Wait and retry with exponential backoff, respecting retry-after.
4xxerrorGeneric code for request errors that do not fall into the cases above (rare).Read message and correct the request.
500internalUnexpected 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 message of errors, keeping the error codes 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 error codes 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.

How to write a client that does not break
  • 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 null and a missing field as equivalent when reading.

Changelog

VersionDateNotes
v1Jul 2026Compatible 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).
v12026First 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 error code and response message;
  • the prefix of the key used (e.g. vfk_9f2c).
Never in the message

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.