Ir al contenido principal

API Reference

A JSON REST API for your schedules, events, sales and fan content. API access is a Pro feature: on the hosted service the schedules you read and write have to be on a Pro or Enterprise plan. A selfhosted install counts as Enterprise, so nothing here is held back by plan.

Authentication

Every endpoint except Register and Login authenticates with an API key sent in the X-API-Key header. There are two ways to get one:

  • Open Settings, go to API Settings and turn on Enable API Access. The key is shown once, so copy it before you leave the page. See Account Settings.
  • Call the Register or Login endpoints, which return a key in the response body (useful for AI agents and scripted setup).

A key belongs to a user account, not to one schedule. It can reach every schedule where you are the owner or an admin, and nothing else. Followers and members cannot be used to authorise API calls.

Plan requirement

On the hosted service, a schedule must be on a Pro or Enterprise plan for the API to see it. Free schedules are filtered out of the list endpoints and return 403 API usage is limited to Pro accounts on the single-record endpoints. There are two deliberate exceptions, so a new account can bootstrap and a free schedule can still take a door sale: Create Schedule and Create Sale. Selfhosted installs resolve to Enterprise, so every endpoint is available there.

Key lifetime and rotation

  • A key expires one year after it is issued. After that every request returns 401 API key expired.
  • Keys are stored hashed, so a lost key cannot be recovered. To rotate one, turn Enable API Access off and back on in Settings. That revokes the old key immediately and issues a new one.
  • Ten consecutive requests with the same invalid key block that key for 15 minutes with 423 API key temporarily blocked.
Keep the key server-side

An API key carries full owner and admin rights over your schedules, including sales data and buyer email addresses. Never ship it in client-side code, a mobile app bundle or a public repository.

cURL Example
curl -X GET "https://mvstudio.com.ar/api/schedules" \
         -H "X-API-Key: your_api_key_here"

Rate Limits

Authenticated requests are counted per IP address, in separate read and write buckets, over a rolling minute:

Operation TypeLimitHTTP Methods
Read operations300 requests/minuteGET
Write operations30 requests/minutePOST, PUT, DELETE

Create Event carries a second throttle of 30 requests per minute on top of the write bucket, so a bulk import should pace itself well below that.

Unauthenticated endpoints

The auth endpoints are limited separately, because they run before any key exists:

EndpointLimitCounted per
/api/register/send-code5 codes per hourEmail address
/api/register3 registrations per hourIP address
/api/login5 failed attempts per 15 minutesIP address

Every one of these returns 429 with an error message when the limit is hit. There are no rate limit headers on the response, so back off on the status code.

Rate Limit Response (429)
{
        "error": "Rate limit exceeded"
    }

Response Format

Every response is JSON. Successful responses wrap the result in a data property: an object for single-record endpoints, an array for list endpoints. List endpoints add a meta object with the pagination counters, and the write endpoints put their confirmation message in meta.message.

Failures return an error string. A validation failure adds an errors object keyed by field name, each holding an array of messages.

Record IDs are opaque strings

Schedules, events, sub-schedules, tickets and sales are all identified by an encoded string such as "evt123", never by the raw database number. Pass the same string back exactly as you received it. Category IDs are the one exception: they are plain integers.

Success Response
{
        "data": [...],
        "meta": {
            "current_page": 1,
            "total": 50
        }
    }
Error Response
{
        "error": "Validation failed",
        "errors": {
            "name": ["The name field is required."]
        }
    }

Pagination

Every list endpoint takes the same two query parameters:

ParameterDefaultDescription
page1Page number to retrieve
per_page100Items per page, maximum 500. Events, sales, feedback and fan content reject a larger value with a 422; schedules clamp it to 500.

The meta object

Every list response returns the same seven counters. Keep requesting page + 1 until it equals last_page.

FieldDescription
current_pageThe page you just received
last_pageThe final page number for this query
per_pagePage size actually applied
totalTotal matching records across all pages
from, to1-based index of the first and last record on this page, or null when the page is empty
pathThe request URL without its query string
cURL
curl -X GET "https://mvstudio.com.ar/api/events?page=2&per_page=50" \
         -H "X-API-Key: your_api_key_here"

Register

Create a new account and receive an API key. No verification code is involved on a selfhosted install, and /api/register/send-code returns a 400 there.

POST /api/register

No authentication required. Rate limited to 3 registrations per IP per hour. On success it returns 201 with an API key that is valid for one year, and the account's email is treated as verified.

ParameterRequiredDescription
nameYesYour display name
emailYesEmail address
passwordYesPassword (min 8 characters)
timezoneNoIANA timezone name (default: America/New_York)
language_codeNoOne of the supported interface languages (default: en)

The endpoint also watches a hidden website honeypot field. Leave it out entirely: sending any value in it returns a 422.

Registration closes after the first account

On a selfhosted install, once any account exists this endpoint returns 403 Registration is closed, unless the install has opted in to open sign-ups with ALLOW_REGISTRATION. An existing account can still get a key from Login or from Settings.

Response (201)
{
        "data": {
            "api_key": "your_new_api_key",
            "api_key_expires_at": "2027-02-28T00:00:00Z",
            "user": {
                "id": "abc123",
                "name": "Your Name",
                "email": "user@example.com"
            }
        }
    }

Login

POST /api/login

No authentication required. Exchanges an email and password for an API key valid for one year.

Login issues a key only when you do not already have one

This is not a session endpoint and it will not hand you a fresh key on demand. If the account already has an unexpired key, login returns 409 and issues nothing, so store the key from the first call. To replace a key you have lost, turn Enable API Access off and back on in Settings.

Two other refusals to handle: an account with two-factor authentication enabled returns 403 and must generate its key from Settings instead, and a wrong email or password returns 401 and counts toward the 5-per-15-minute limit.

ParameterRequiredDescription
emailYesEmail address
passwordYesPassword
cURL
curl -X POST "https://mvstudio.com.ar/api/login" \
         -H "Content-Type: application/json" \
         -d '{"email": "user@example.com", "password": "your_password"}'
Response (200)
{
        "data": {
            "api_key": "your_new_api_key",
            "api_key_expires_at": "2027-02-28T00:00:00Z",
            "user": {
                "id": "abc123",
                "name": "Your Name",
                "email": "user@example.com"
            }
        }
    }

List Schedules

GET /api/schedules

Returns a paginated list of the schedules where you are the owner or an admin. Deleted schedules are excluded, and on the hosted service so are schedules that are not on a Pro or Enterprise plan. Each row carries the schedule's sub-schedules in a groups array.

ParameterDescription
subdomainFilter by exact subdomain
nameFilter by schedule name (partial match)
typeFilter by type: venue, talent, or curator
cURL
curl -X GET "https://mvstudio.com.ar/api/schedules?type=venue" \
         -H "X-API-Key: your_api_key_here"
Response (200)
{
        "data": [
            {
                "id": "abc123",
                "subdomain": "my-venue",
                "name": "My Venue",
                "type": "venue",
                "email": "info@myvenue.com",
                "timezone": "America/New_York",
                ...
            }
        ],
        "meta": { "current_page": 1, "total": 5 }
    }

Show Schedule

GET /api/schedules/{subdomain}

Returns a single schedule by subdomain, including its sub-schedules in a groups array. You must be the owner or an admin of it, otherwise the response is 404. A schedule that is not on a Pro or Enterprise plan returns 403.

cURL
curl -X GET "https://mvstudio.com.ar/api/schedules/my-venue" \
         -H "X-API-Key: your_api_key_here"
Response (200)
{
        "data": {
            "id": "abc123",
            "subdomain": "my-venue",
            "name": "My Venue",
            "type": "venue",
            "groups": [
                { "id": "def456", "name": "Main Stage", "slug": "main-stage" }
            ],
            ...
        }
    }

Create Schedule

POST /api/schedules

Create a new schedule. This is the one write endpoint with no plan gate, so a new account can bootstrap itself. Every other endpoint then needs that schedule to be on a Pro or Enterprise plan, so on the hosted service subscribe before you start pushing events. You are attached to the new schedule as its owner, and it becomes your default schedule if you had none.

ParameterRequiredDescription
nameYesSchedule name (max 255 characters). The subdomain is generated from it and cannot be set through the API.
typeYesSchedule type: venue, talent, or curator
emailNoContact email
descriptionNoMarkdown description (max 10,000 characters)
short_descriptionNoOne-line summary (max 200 characters)
timezoneNoIANA timezone name (defaults to your account timezone)
language_codeNoSupported language code such as en, es, fr (defaults to your account language)
websiteNoWebsite URL
address1, city, state, postal_code, country_codeNoAddress fields, used for venue schedules. Send country_code as a two-letter ISO code.

On the hosted service one account may own up to 50 schedules. Beyond that the endpoint returns a 422.

cURL
curl -X POST "https://mvstudio.com.ar/api/schedules" \
         -H "X-API-Key: your_api_key_here" \
         -H "Content-Type: application/json" \
         -d '{"name": "My Venue", "type": "venue", "city": "New York"}'

Update Schedule

PUT /api/schedules/{subdomain}

Update a schedule. Include only the fields you want to change; anything you omit is left alone. Takes the same fields as Create Schedule apart from type: neither the schedule type nor the subdomain can be changed through the API. Requires owner or admin access and a Pro or Enterprise plan.

Branding, images, layout and integrations are not exposed here. Edit those in the admin panel, under the Style, Settings and Integrations sections of the schedule editor.

cURL
curl -X PUT "https://mvstudio.com.ar/api/schedules/my-venue" \
         -H "X-API-Key: your_api_key_here" \
         -H "Content-Type: application/json" \
         -d '{"name": "Updated Name", "description": "New description"}'

Delete Schedule

DELETE /api/schedules/{subdomain}

Retire a schedule. Requires owner access: an admin gets a 404. There is no undo through the API.

The schedule is flagged as deleted and stops appearing anywhere, and the call also:

  • Deletes its profile, header and background images from storage
  • Deletes its analytics history (page views, referrers and appearances)
  • Tears down its Google Calendar and Outlook sync subscriptions
  • Cancels any running boost campaign and refunds it where money is owed
  • Emails the schedule's members to tell them it was deleted

Events are not swept up automatically. The exception is a talent schedule: an event whose only member was that schedule is deleted with it, so nothing is left orphaned.

cURL
curl -X DELETE "https://mvstudio.com.ar/api/schedules/my-venue" \
         -H "X-API-Key: your_api_key_here"
Response (200)
{
        "data": {
            "message": "Schedule deleted successfully"
        }
    }

List Sub-Schedules

GET /api/schedules/{subdomain}/groups

List every sub-schedule on a schedule, returning id, name, slug and color for each. Requires owner or admin access and a Pro or Enterprise plan. The response is not paginated.

Sub-schedules group and colour-code events so visitors can filter your calendar. They do not control who can see an event: use the visibility flags on Create Event for that.

cURL
curl -X GET "https://mvstudio.com.ar/api/schedules/my-venue/groups" \
         -H "X-API-Key: your_api_key_here"
Response (200)
{
        "data": [
            {
                "id": "def456",
                "name": "Main Stage",
                "slug": "main-stage",
                "color": "#FF5733"
            }
        ]
    }

Create Sub-Schedule

POST /api/schedules/{subdomain}/groups

Create a sub-schedule on a schedule. Requires owner or admin access and a Pro or Enterprise plan. The slug is generated from the name and is what you pass as the schedule parameter when creating an event.

ParameterRequiredDescription
nameYesSub-schedule name (max 255 characters)
colorNoDisplay colour as a hex value, for example #FF5733 (max 50 characters)

If the schedule has a translation language set that differs from its own language, the name is machine-translated into it and the slug is built from the translated name, so read the slug back from the response rather than deriving it yourself.

cURL
curl -X POST "https://mvstudio.com.ar/api/schedules/my-venue/groups" \
         -H "X-API-Key: your_api_key_here" \
         -H "Content-Type: application/json" \
         -d '{"name": "Main Stage", "color": "#FF5733"}'
Response (201)
{
        "data": {
            "id": "def456",
            "name": "Main Stage",
            "slug": "main-stage",
            "color": "#FF5733"
        }
    }

Update Sub-Schedule

PUT /api/schedules/{subdomain}/groups/{group_id}

Update a sub-schedule's name or colour; send only the one you want to change. Requires owner or admin access and a Pro or Enterprise plan. Changing the name regenerates the slug, which changes the value events must pass in schedule, so re-read it from the response.

cURL
curl -X PUT "https://mvstudio.com.ar/api/schedules/my-venue/groups/def456" \
         -H "X-API-Key: your_api_key_here" \
         -H "Content-Type: application/json" \
         -d '{"name": "VIP Stage", "color": "#3B82F6"}'

Delete Sub-Schedule

DELETE /api/schedules/{subdomain}/groups/{group_id}

Delete a sub-schedule. Events assigned to it are kept and simply lose the assignment. Requires owner or admin access and a Pro or Enterprise plan. Pass the encoded sub-schedule id from List Sub-Schedules.

cURL
curl -X DELETE "https://mvstudio.com.ar/api/schedules/my-venue/groups/def456" \
         -H "X-API-Key: your_api_key_here"
Response (200)
{
        "data": {
            "message": "Sub-schedule deleted successfully"
        }
    }

List Events

GET /api/events

Returns a paginated list of events on the schedules where you are the owner or an admin, newest start date first. On the hosted service an event is only listed if at least one of its schedules is on a Pro or Enterprise plan. Appointment bookings are never returned here; they are not calendar events.

Drafts, internal and unlisted events are all included, so check is_draft, is_internal and is_private before republishing a row on a public site.

ParameterDescription
subdomainFilter events by schedule subdomain
starts_afterEvents starting on or after this UTC date (Y-m-d)
starts_beforeEvents starting on or before this UTC date (Y-m-d)
venue_idFilter by venue (encoded venue schedule ID)
category_idFilter by category ID (integer, see List Categories)
nameFilter by event name (partial match)
schedule_typeFilter by type: single or recurring
tickets_enabledFilter by whether tickets are enabled (boolean)
rsvp_enabledFilter by whether RSVP/registration is enabled (boolean)
group_idFilter by sub-schedule (encoded sub-schedule ID)
All times are UTC

The API reads and writes starts_at in UTC, in Y-m-d H:i:s format with no offset suffix. The schedule's own timezone only controls how that instant is displayed on the guest page, so convert on your side before filtering or creating.

cURL
curl -X GET "https://mvstudio.com.ar/api/events?subdomain=my-venue&starts_after=2025-01-01" \
         -H "X-API-Key: your_api_key_here"
Response (200)
{
        "data": [
            {
                "id": "evt123",
                "name": "Jazz Night",
                "starts_at": "2025-03-15 20:00:00",
                "duration": 3,
                "tickets_enabled": true,
                "rsvp_enabled": false,
                ...
            }
        ],
        "meta": { "current_page": 1, "total": 25 }
    }

Show Event

GET /api/events/{id}

Returns a single event by its encoded ID, including its ticket types, add-ons, members, agenda parts, venue, recurring configuration and visibility flags. Requires owner or admin access on one of the event's schedules, and a Pro or Enterprise plan.

cURL
curl -X GET "https://mvstudio.com.ar/api/events/evt123" \
         -H "X-API-Key: your_api_key_here"
Response (200)
{
        "data": {
            "id": "evt123",
            "name": "Jazz Night",
            "starts_at": "2025-03-15 20:00:00",
            "duration": 3,
            "tickets": [
                { "id": "tkt1", "type": "General", "price": 25, "quantity": 100 }
            ],
            "event_parts": [
                { "name": "Opening Act", "start_time": "20:00" }
            ],
            ...
        }
    }

Create Event

POST /api/events/{subdomain}

Create an event on the schedule identified by {subdomain}. Requires owner or admin access on that schedule and a Pro or Enterprise plan. This endpoint carries its own throttle of 30 requests per minute in addition to the write bucket.

Core fields

ParameterRequiredDescription
nameYesEvent name (max 255 characters)
starts_atYesStart date and time in UTC, formatted Y-m-d H:i:s
durationNoLength in hours, 0 to 8760. Decimals are allowed, so 1.5 is 90 minutes. There is no separate end-time field.
descriptionNoFull description, Markdown supported (max 10,000 characters)
short_descriptionNoShort description used in listings and previews (max 500 characters)
event_urlNoA single URL for an online event or an external event page (max 255 characters)
registration_urlNoExternal registration URL, used instead of on-platform tickets (max 2048 characters)
category_idNoCategory ID, which must be in this schedule's effective category list (see List Categories)
categoryNoCategory name, matched case- and punctuation-insensitively against the same list. Ignored when category_id is present; an unmatched name returns 422 Category not found.
scheduleNoSub-schedule slug to file the event under. An unknown slug returns 422 Sub-schedule not found.

Visibility

Send no visibility flag at all and the event inherits the schedule's default for new events. Unlisted and Internal need an Enterprise plan Enterprise - Requires the Enterprise plan ; on any lower plan they are stripped and the event is saved as a Draft so it never publishes by accident.

The draft default is applied even to a partial request that omits is_draft, so on a drafts-by-default schedule you must send is_draft: false to publish straight away.

ParameterRequiredDescription
is_draftNoDraft: visible to your team in the admin panel, hidden from the public page (boolean)
is_privateNoUnlisted: kept off the calendar but reachable by direct link (boolean). Enterprise only.
is_internalNoInternal: never public, and mutually exclusive with Unlisted (boolean). Enterprise only.
event_passwordNoPassword prompt on the event page. Only applies to an Unlisted event, and is discarded otherwise.

Recurrence

ParameterRequiredDescription
schedule_typeNosingle (default) or recurring
recurring_frequencyWith recurringdaily, weekly, every_n_weeks, monthly_date, monthly_weekday, or yearly
days_of_weekWith weeklySeven characters of 0 or 1, Sunday to Saturday. "0101010" is Monday, Wednesday and Friday. Required for weekly and every_n_weeks.
recurring_intervalNoWeek gap for every_n_weeks (integer, minimum 2)
recurring_end_typeNonever, on_date, or after_events
recurring_end_valueNoEnd date (Y-m-d) for on_date, or the number of occurrences for after_events

Tickets, RSVP and add-ons

ParameterRequiredDescription
rsvp_enabledNoEnable free registration, which collects a name and email without a payment step (boolean)
rsvp_limitNoCap on registrations per date (integer, minimum 1)
tickets_enabledNoEnable ticketing (boolean)
ticket_currency_codeNoThree-letter ISO currency code, for example USD
payment_methodNocash, stripe, invoiceninja, payment_url or payfast. manual is accepted as an alias for cash. The method must be connected on the account, and payfast only settles events priced in ZAR. On create, omitting this field uses the installation's DEFAULT_PAYMENT_METHOD if one is set and is usable for the event's currency, falling back to cash; send null to mean cash explicitly. On update, omitting it leaves the stored value alone
payment_instructionsNoInstructions shown for manual payment (max 5000 characters)
ticketsNoArray of ticket types. Each takes type (required), quantity, price, description, sales_start_at and sales_end_at. A quantity of 0 means unlimited.
addonsNoArray of paid extras sold alongside a ticket, such as parking or merchandise. Each takes type (required), quantity, price, description and url. Only saved when tickets_enabled is true.

Agenda, venue and performers

ParameterRequiredDescription
event_partsNoAgenda segments within the event. Each takes name (required), description, start_time and end_time.
venue_idNoEncoded ID of an existing venue schedule
venue_nameNoVenue name. Must be sent together with venue_address1.
venue_address1NoVenue street address. The pair is looked up against venue schedules you own or follow; no match returns 422 Venue not found rather than creating one.
membersNoPerformers, given as objects with name and/or email. Each is matched to an existing talent schedule you own or follow; no match returns 422 Talent member not found.
The schedule's own type is applied for you

Creating on a venue schedule sets that venue on the event, creating on a talent schedule adds it as a member, and creating on a curator schedule lists the event as curated. You do not need to send venue_id or members for the schedule you are posting to.

The hosted service also applies a generous daily cap on how many events one schedule or one account may create, as an anti-abuse measure. A bulk import that trips it gets a 422 and can resume the next day. Selfhosted installs have no cap.

cURL
curl -X POST "https://mvstudio.com.ar/api/events/my-venue" \
         -H "X-API-Key: your_api_key_here" \
         -H "Content-Type: application/json" \
         -d '{
             "name": "Jazz Night",
             "starts_at": "2026-09-04 20:00:00",
             "duration": 3,
             "description": "A wonderful evening of jazz music.",
             "tickets_enabled": true,
             "tickets": [
                 {"type": "General Admission", "price": 25, "quantity": 100},
                 {"type": "VIP", "price": 50, "quantity": 20}
             ],
             "event_parts": [
                 {"name": "Opening Act", "start_time": "20:00", "end_time": "20:45"},
                 {"name": "Main Performance", "start_time": "21:00", "end_time": "23:00"}
             ]
         }'

Update Event

PUT /api/events/{id}

Update an event by its encoded ID. Takes the same parameters as Create Event, and supports partial updates: send only the fields you want to change. Requires owner or admin access on one of the event's schedules and a Pro or Enterprise plan.

Omitting a collection leaves it alone. The start time, recurring configuration, ticket types, add-ons and agenda parts are all carried over from the stored event when the request does not mention them.

A collection you do send replaces the whole list

Sending tickets, addons or event_parts replaces that whole list: any row you leave out is retired. To change one ticket type, send the full set with your edit applied. Sending tickets_enabled: false retires every ticket type on the event.

cURL
curl -X PUT "https://mvstudio.com.ar/api/events/evt123" \
         -H "X-API-Key: your_api_key_here" \
         -H "Content-Type: application/json" \
         -d '{"name": "Updated Jazz Night", "duration": 4}'

Delete Event

DELETE /api/events/{id}

Permanently delete an event. Requires owner or admin access on one of its schedules and a Pro or Enterprise plan. There is no undo, so hide the event with is_draft instead if you may want it back.

Deleting also removes the synced copy from any connected Google Calendar, Outlook calendar and CalDAV calendar, cancels any running boost campaign, and deletes its sponsor logo files. Unless the event was a draft, an event.deleted webhook is sent with the event's final state.

cURL
curl -X DELETE "https://mvstudio.com.ar/api/events/evt123" \
         -H "X-API-Key: your_api_key_here"
Response (200)
{
        "data": {
            "message": "Event deleted successfully"
        }
    }

Upload Flyer

POST /api/events/flyer/{event_id}

Set the flyer image for an event. Send it as multipart/form-data in a flyer_image field, not as JSON. Requires owner or admin access on one of the event's schedules and a Pro or Enterprise plan.

ConstraintValue
Formatsjpg, jpeg, png, gif, webp
Maximum size10 MB
Existing flyerReplaced, and the old file is deleted from storage

The response is the full event record, so you can read the new flyer_image_url straight back from data. There is no endpoint for removing a flyer.

cURL
curl -X POST "https://mvstudio.com.ar/api/events/flyer/evt123" \
         -H "X-API-Key: your_api_key_here" \
         -F "flyer_image=@/path/to/flyer.jpg"
Response (200)
{
        "data": { ... },
        "meta": {
            "message": "Flyer uploaded successfully"
        }
    }

List Categories

GET /api/categories

Returns the built-in event categories with their integer IDs and English names. Pass an id as category_id when creating or updating an event. The list is not paginated.

Categories for one schedule

GET /api/categories/{subdomain}

A schedule can rename, hide or add categories of its own, and category_id is validated against that effective list rather than the global one. Call this variant to get the exact set a given schedule will accept, and use it whenever the schedule has customised its categories.

cURL
curl -X GET "https://mvstudio.com.ar/api/categories" \
         -H "X-API-Key: your_api_key_here"
Response (200)
{
        "data": [
            {"id": 1, "name": "Art & Culture"},
            {"id": 2, "name": "Business Networking"},
            {"id": 3, "name": "Community"},
            {"id": 4, "name": "Concerts"},
            ...
        ]
    }

List Sales

GET /api/sales

Returns a paginated list of sales on events you own or administer, newest order first. Deleted sales are excluded, and on the hosted service so are sales on schedules that are not Pro or Enterprise. RSVP registrations appear here too, as zero-value paid sales.

ParameterDescription
event_idFilter by event (encoded event ID)
subdomainFilter by schedule subdomain
statusFilter by status: unpaid, paid, cancelled, refunded, or expired
emailFilter by buyer email (exact match)
event_dateFilter by event date (Y-m-d)
cURL
curl -X GET "https://mvstudio.com.ar/api/sales?status=paid&subdomain=my-venue" \
         -H "X-API-Key: your_api_key_here"
Response (200)
{
        "data": [
            {
                "id": "sale123",
                "event_name": "Jazz Night",
                "name": "John Doe",
                "email": "john@example.com",
                "status": "paid",
                "payment_amount": 50,
                "tickets": [
                    { "type": "General", "quantity": 2, "price": 25 }
                ],
                ...
            }
        ],
        "meta": { "current_page": 1, "total": 12 }
    }

Show Sale

GET /api/sales/{id}

Returns a single sale by its encoded ID, including a row per ticket type and add-on with ticket_id, type, quantity, price and the is_addon and is_pass flags. Requires owner or admin access on the event's schedule and a Pro or Enterprise plan.

An order bought for several named guests is stored as one row per guest, all sharing a group_id. The row with is_primary set to true holds the totals for the whole order; the other rows report zero so you do not double-count when you add them up. Every row in a group belongs to the same event.

A purchase that covered several events shares an order_id instead, one row per event, with is_order_primary on the anchoring row. The two nest: a leg of an order can itself be split across named guests, so a row may carry both.

payment_amount is what the buyer agreed to pay, not what has been collected. The two differ for a sale bought on an installment plan: the sale reads paid with the full total from the first payment onwards, because the ticket is issued then, while the rest arrives over the following months. Reconcile against Stripe rather than against this field if you are counting money in the bank.

cURL
curl -X GET "https://mvstudio.com.ar/api/sales/sale123" \
         -H "X-API-Key: your_api_key_here"
Response (200)
{
        "data": {
            "id": "sale123",
            "event_id": "evt123",
            "event_name": "Jazz Night",
            "name": "John Doe",
            "email": "john@example.com",
            "status": "paid",
            "payment_amount": 50,
            "total_quantity": 2,
            "tickets": [
                { "type": "General", "quantity": 2, "price": 25 }
            ]
        }
    }

Create Sale

POST /api/sales

Record a sale against an event, for example when someone paid you at the door or through a channel Event Schedule does not handle. The event must have ticketing enabled and still be selling. This is one of the two endpoints with no Pro gate, so a free schedule can use it within its monthly paid-ticket allowance; the resulting sale will not appear in List Sales until the schedule is on a paid plan.

ParameterRequiredDescription
event_idYesEncoded event ID
nameYesBuyer name (max 255 characters)
emailYesBuyer email (max 255 characters)
ticketsYesObject mapping ticket identifiers to quantities, each 1 or more. A key may be an encoded ticket ID or a ticket type name.
addonsNoObject mapping encoded add-on IDs to quantities
event_dateNoWhich date of the event the sale is for (Y-m-d). Defaults to the event's start date, and is required for a recurring event.

What happens on success

  • The sale is created as unpaid. You cannot set the status from the request; use Update Sale Status once you have the money.
  • A sale whose total comes to zero is marked paid immediately.
  • Any volume discount configured on the ticket type is applied to the total.
  • A sale.created webhook fires, plus sale.paid for a zero-total sale.

Inventory is checked under a lock, so you cannot oversell through this endpoint. Common 422 replies are a past event or occurrence, a ticket whose sales window has not opened or has closed, and a quantity larger than the remaining stock, which reports how many are left.

cURL
curl -X POST "https://mvstudio.com.ar/api/sales" \
         -H "X-API-Key: your_api_key_here" \
         -H "Content-Type: application/json" \
         -d '{
             "event_id": "evt123",
             "name": "John Doe",
             "email": "john@example.com",
             "tickets": {"General Admission": 2}
         }'

Update Sale Status

PUT /api/sales/{id}

Move a sale to a new status by sending an action. Which actions are available depends on where the sale is now; an action the current status does not allow returns a 422 naming both.

ActionFrom StatusTo StatusWebhook
mark_paidunpaidpaidsale.paid
refundpaidrefundedsale.refunded
cancelunpaid, paidcancelledsale.cancelled
refund does not move money

This action records the sale as refunded and backs the amount out of your revenue figures. It does not send anything through Stripe, Invoice Ninja or Payfast. Issue the actual refund in your payment provider, then call this to keep the two in step.

Cancelling or refunding releases the seats back into stock and notifies anyone on the waitlist for that date. For a multi-event order, act on the primary sale: a non-primary row returns 403, and the change cascades to the rest of the order for you.

cURL
curl -X PUT "https://mvstudio.com.ar/api/sales/sale123" \
         -H "X-API-Key: your_api_key_here" \
         -H "Content-Type: application/json" \
         -d '{"action": "mark_paid"}'

Delete Sale

DELETE /api/sales/{id}

Remove a sale from your records. It is cancelled first, so its seats return to stock, and then flagged as deleted: it stops appearing in List Sales and in the admin panel, and Show Sale returns 404 for it. Requires owner or admin access on the event's schedule and a Pro or Enterprise plan.

Deleting the primary sale of a multi-event order deletes the whole order. A non-primary row returns 403.

cURL
curl -X DELETE "https://mvstudio.com.ar/api/sales/sale123" \
         -H "X-API-Key: your_api_key_here"
Response (200)
{
        "data": {
            "message": "Sale deleted successfully"
        }
    }

List Feedback

GET /api/feedback

Returns a paginated list of post-event feedback, meaning the star ratings and comments attendees leave after an event, for schedules you own or administer. Newest first. Read only: there is no endpoint for creating or deleting feedback.

Only feedback attached to a paid, undeleted sale is returned, which is the same rule the guest page applies, so a cancelled or refunded order's rating never shows up here. On the hosted service the schedule must be on a Pro or Enterprise plan, since collecting feedback is itself a Pro feature.

ParameterDescription
event_idFilter by event (encoded event ID)
subdomainFilter by schedule subdomain
event_dateFilter by the date of the event attended (Y-m-d)
min_ratingOnly return ratings of at least this value (1-5)
fromOnly feedback submitted on or after this date (Y-m-d)
toOnly feedback submitted on or before this date (Y-m-d)
Rows carry attendee contact details

Each record includes attendee_name and attendee_email. Strip both before you render feedback on a public page.

cURL
curl -X GET "https://mvstudio.com.ar/api/feedback?min_rating=4&subdomain=my-venue" \
         -H "X-API-Key: your_api_key_here"
Response (200)
{
        "data": [
            {
                "id": "fb123",
                "event_id": "ev456",
                "event_name": "Jazz Night",
                "event_date": "2026-07-10",
                "rating": 5,
                "comment": "Best night out all year",
                "attendee_name": "Alex Attendee",
                "attendee_email": "alex@example.com",
                "created_at": "2026-07-11T09:12:00+00:00"
            }
        ],
        "meta": { "current_page": 1, "total": 8 }
    }

List Fan Content

GET /api/fan-content

Returns fan comments, photos and videos submitted on events for schedules you own or administer, all three kinds merged into one feed, newest first. Approved items only by default, which is what you want when displaying them on an external site. Submitter email addresses are never included. Read only: approve and reject submissions in the admin panel.

Each kind of submission has its own id sequence, so an id is only unique within a type. Key on the two together when storing rows from this feed.

ParameterDescription
typeLimit to one kind: comment, photo, or video
event_idFilter by event (encoded event ID)
subdomainFilter by schedule subdomain
event_dateFilter by event date (Y-m-d)
is_approvedDefaults to true. Pass 0 to read the pending moderation queue instead
cURL
curl -X GET "https://mvstudio.com.ar/api/fan-content?type=photo&subdomain=my-venue" \
         -H "X-API-Key: your_api_key_here"
Response (200)
{
        "data": [
            {
                "id": "ph123",
                "type": "photo",
                "event_id": "ev456",
                "event_name": "Jazz Night",
                "event_date": "2026-07-10",
                "submitted_by": "Dana Guest",
                "is_guest_submission": true,
                "is_approved": true,
                "photo_url": "https://.../crowd.jpg",
                "created_at": "2026-07-11T09:12:00+00:00"
            }
        ],
        "meta": { "current_page": 1, "total": 24 }
    }

Error Handling

The API uses standard HTTP status codes and always returns the reason as a JSON error string.

CodeWhen you see it
200Success
201Created, returned by Register, Create Schedule, Create Sub-Schedule, Create Event and Create Sale
400Verification codes requested on a selfhosted install, where they do not apply
401API key missing, invalid, or past its one-year expiry. Also a wrong email or password on Login.
403You are not an owner or admin of the record, the schedule is not on a Pro or Enterprise plan, the account uses two-factor authentication, or selfhosted registration is closed
404Not found, or found but outside the schedules your key can reach
409Login when the account already has an unexpired API key
422Validation error, with field-level detail in errors. Also business refusals such as an unmatched venue, a sold-out ticket or a past event.
423The API key is blocked for 15 minutes after 10 consecutive failed attempts
429Rate limit exceeded, see Rate Limits
500Server error. Retry with backoff; the failure is logged on our side.

A 422 covers two different things. A schema problem carries an errors object and is worth surfacing field by field; a business refusal carries only error and reads as a sentence. Check for errors before assuming its shape.

Validation Error (422)
{
        "error": "Validation failed",
        "errors": {
            "name": ["The name field is required."],
            "starts_at": ["The starts at must match the format Y-m-d H:i:s."]
        }
    }

See Also