{"openapi":"3.1.0","info":{"title":"Fevori API","version":"1.0.0","description":"Manage your Fevori events and guest lists from scripts and AI agents. Every request acts as one account: send an API key (created at https://fevori.app/account; it can do everything the account can) or an OAuth access token for resource https://fevori.app (the person signs in with Google and chooses what it may do; see the oauth2 scheme).\n\nAn AI agent without a key can make its own account with POST /accounts. It gets a key and a claim_url to give its person. Until they open the link and sign in with Google, events stay drafts and invitations can't be sent; once they do, the account's events move into theirs and the same key works for their account. Unclaimed accounts are deleted after 30 days.\n\nInvitations go by email. Texts are sent only from the event page, where the host confirms each guest agreed to get them.\n\nThe same operations are available as an MCP server at https://fevori.app/mcp, which accepts OAuth (sign in with Google) or an API key.","contact":{"email":"support@fevori.app"},"termsOfService":"https://fevori.app/terms"},"servers":[{"url":"https://fevori.app/api/v1"}],"externalDocs":{"url":"https://fevori.app/docs/api"},"security":[{"apiKey":[]},{"oauth2":["events:read"]}],"tags":[{"name":"Account"},{"name":"Events"},{"name":"Guests"},{"name":"Invitations"}],"paths":{"/events":{"get":{"tags":["Events"],"operationId":"listEvents","summary":"List your events","description":"Undated events last; otherwise latest start first.","responses":{"200":{"description":"Your events.","content":{"application/json":{"schema":{"type":"object","properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/Event"}}},"required":["events"]}}}},"401":{"description":"Missing, unknown or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"apiKey":[]},{"oauth2":["events:read"]}]},"post":{"tags":["Events"],"operationId":"createEvent","summary":"Create an event","description":"An unclaimed agent account can only make drafts, and at most 25 events.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventInput","required":["title"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventEnvelope"}}}},"400":{"description":"The body is not a JSON object, or a field is invalid. `details` lists each problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"This is an agent account no person has claimed yet (code account_unclaimed). The error includes the claim_url to give them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"apiKey":[]},{"oauth2":["events:read","events:write"]}]}},"/events/{eventId}":{"parameters":[{"name":"eventId","in":"path","required":true,"schema":{"type":"string"}}],"get":{"tags":["Events"],"operationId":"getEvent","summary":"Get an event","responses":{"200":{"description":"The event.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventEnvelope"}}}},"401":{"description":"Missing, unknown or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such event (or guest), or it belongs to someone else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"apiKey":[]},{"oauth2":["events:read"]}]},"patch":{"tags":["Events"],"operationId":"updateEvent","summary":"Change an event","description":"Send only the fields to change; null clears an optional field. An unclaimed agent account can’t publish.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventInput"}}}},"responses":{"200":{"description":"The updated event.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventEnvelope"}}}},"400":{"description":"The body is not a JSON object, or a field is invalid. `details` lists each problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"This is an agent account no person has claimed yet (code account_unclaimed). The error includes the claim_url to give them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such event (or guest), or it belongs to someone else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"apiKey":[]},{"oauth2":["events:read","events:write"]}]},"delete":{"tags":["Events"],"operationId":"deleteEvent","summary":"Delete an event","description":"Also deletes its guests and their invite links. Cannot be undone.","responses":{"204":{"description":"Deleted."},"401":{"description":"Missing, unknown or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such event (or guest), or it belongs to someone else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"apiKey":[]},{"oauth2":["events:read","events:write"]}]}},"/events/{eventId}/guests":{"parameters":[{"name":"eventId","in":"path","required":true,"schema":{"type":"string"}}],"get":{"tags":["Guests"],"operationId":"listGuests","summary":"List an event's guests","description":"In the order they were added.","parameters":[{"name":"status","in":"query","required":false,"schema":{"$ref":"#/components/schemas/GuestStatus"},"description":"Only guests with this status."}],"responses":{"200":{"description":"The guest list.","content":{"application/json":{"schema":{"type":"object","properties":{"guests":{"type":"array","items":{"$ref":"#/components/schemas/Guest"}}},"required":["guests"]}}}},"400":{"description":"The body is not a JSON object, or a field is invalid. `details` lists each problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such event (or guest), or it belongs to someone else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"apiKey":[]},{"oauth2":["events:read"]}]},"post":{"tags":["Guests"],"operationId":"addGuest","summary":"Add a guest","description":"Adds the guest and makes their personal invite link. Nothing is sent.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestInput","required":["name"]}}}},"responses":{"201":{"description":"Added.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestEnvelope"}}}},"400":{"description":"The body is not a JSON object, or a field is invalid. `details` lists each problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such event (or guest), or it belongs to someone else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Someone on this guest list already has that email (code email_taken).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"apiKey":[]},{"oauth2":["events:read","events:write"]}]}},"/events/{eventId}/guests/{guestId}":{"parameters":[{"name":"eventId","in":"path","required":true,"schema":{"type":"string"}},{"name":"guestId","in":"path","required":true,"schema":{"type":"string"}}],"get":{"tags":["Guests"],"operationId":"getGuest","summary":"Get a guest","responses":{"200":{"description":"The guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestEnvelope"}}}},"401":{"description":"Missing, unknown or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such event (or guest), or it belongs to someone else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"apiKey":[]},{"oauth2":["events:read"]}]},"patch":{"tags":["Guests"],"operationId":"updateGuest","summary":"Change a guest","description":"Send only the fields to change. Their invite link stays the same.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestInput"}}}},"responses":{"200":{"description":"The updated guest.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GuestEnvelope"}}}},"400":{"description":"The body is not a JSON object, or a field is invalid. `details` lists each problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such event (or guest), or it belongs to someone else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Someone else on this guest list already has that email (code email_taken).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"apiKey":[]},{"oauth2":["events:read","events:write"]}]},"delete":{"tags":["Guests"],"operationId":"removeGuest","summary":"Remove a guest","description":"Their invite link stops working. Cannot be undone.","responses":{"204":{"description":"Removed."},"401":{"description":"Missing, unknown or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such event (or guest), or it belongs to someone else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"apiKey":[]},{"oauth2":["events:read","events:write"]}]}},"/events/{eventId}/invitations":{"parameters":[{"name":"eventId","in":"path","required":true,"schema":{"type":"string"}}],"post":{"tags":["Invitations"],"operationId":"sendInvitations","summary":"Email invitations","description":"Queues an invitation email to each guest, with their personal link. Without guest_ids, goes to every guest with an email address who hasn’t been emailed yet; with guest_ids, to those guests, again if already emailed. The event must be published, and the account claimed by a person. Guests with no email, who unsubscribed, or with an email already on its way are skipped.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"guest_ids":{"type":"array","items":{"type":"string"},"maxItems":1000},"message":{"type":["string","null"],"maxLength":2000,"description":"A personal note added to the email."}},"additionalProperties":false}}}},"responses":{"202":{"description":"Queued. Each guest’s status moves from sending to pending as the email goes out.","content":{"application/json":{"schema":{"type":"object","properties":{"queued":{"type":"integer"},"guest_ids":{"type":"array","items":{"type":"string"},"description":"Guests being emailed."},"skipped":{"type":"array","items":{"type":"object","properties":{"guest_id":{"type":"string"},"name":{"type":"string"},"reason":{"type":"string"}},"required":["guest_id","name","reason"]}}},"required":["queued","guest_ids","skipped"]}}}},"400":{"description":"The body is not a JSON object, or a field is invalid. `details` lists each problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, unknown or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"This is an agent account no person has claimed yet (code account_unclaimed). The error includes the claim_url to give them.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such event (or guest), or it belongs to someone else.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The event isn’t published yet (code event_not_published).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"apiKey":[]},{"oauth2":["events:read","invitations:send"]}]}},"/accounts":{"post":{"tags":["Account"],"operationId":"createAgentAccount","summary":"Make an agent account","description":"For an AI agent with no key: makes a temporary account and returns its API key (shown once) and a claim_url. Give the claim_url to your person; once they sign in with Google there, the account's events move into theirs and the same key works for their account. Until then, events stay drafts and invitations can't be sent. Unclaimed accounts are deleted after 30 days. A few per minute per IP address.","security":[],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","maxLength":100,"description":"Who made it, shown to the person claiming it. Default \"AI agent\"."}},"additionalProperties":false}}}},"responses":{"201":{"description":"Made.","content":{"application/json":{"schema":{"type":"object","properties":{"api_key":{"type":"string","description":"Send as \"Authorization: Bearer <api_key>\". Not shown again."},"claim_url":{"type":"string","format":"uri"},"account":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"claimed":{"const":false},"expires_at":{"type":"string","format":"date-time"}}}},"required":["api_key","claim_url","account"]}}}},"400":{"description":"The body is not a JSON object, or a field is invalid. `details` lists each problem.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many accounts made from this address; try again in a minute (code rate_limited).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/me":{"get":{"tags":["Account"],"operationId":"getMe","summary":"Who this key acts as","description":"Includes whether the account has been claimed, and its claim_url if not.","responses":{"200":{"description":"The key’s account.","content":{"application/json":{"schema":{"type":"object","properties":{"user":{"$ref":"#/components/schemas/User"}},"required":["user"]}}}},"401":{"description":"Missing, unknown or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"apiKey":[]},{"oauth2":["events:read"]}]}},"/health":{"get":{"tags":["Account"],"operationId":"health","summary":"Is the API up","security":[],"responses":{"200":{"description":"Up.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"const":"ok"}}}}}}}}}},"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"An API key from https://fevori.app/account, sent as \"Authorization: Bearer fev_…\". It can do everything the account can."},"oauth2":{"type":"oauth2","description":"OAuth 2.1 with PKCE; the person signs in with Google and chooses permissions. Request resource=https://fevori.app. Clients can register themselves (https://fevori.app/oauth/register) or use a Client ID Metadata Document. Discovery: https://fevori.app/.well-known/oauth-protected-resource.","flows":{"authorizationCode":{"authorizationUrl":"https://fevori.app/oauth/authorize","tokenUrl":"https://fevori.app/oauth/token","refreshUrl":"https://fevori.app/oauth/token","scopes":{"events:read":"See your events, guest lists and RSVPs","events:write":"Create, change and delete events and guests","invitations:send":"Email invitations to your guests in your name"}}}}},"schemas":{"Event":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string","maxLength":200},"description":{"type":["string","null"],"maxLength":5000,"description":"What to bring, parking, dress code…"},"starts_at":{"type":["string","null"],"format":"date-time","description":"An instant with an offset, e.g. 2026-06-06T19:00:00-07:00. Returned in UTC."},"ends_at":{"type":["string","null"],"format":"date-time","description":"After starts_at; needs starts_at."},"timezone":{"type":"string","description":"IANA time zone the event happens in, used to show its times to guests. Default UTC.","examples":["America/Los_Angeles"]},"location_name":{"type":["string","null"],"maxLength":200},"location_address":{"type":["string","null"],"maxLength":500},"visibility":{"type":"string","enum":["private","public"],"description":"public: anyone can open the event page and RSVP, and it is listed for search engines. Default private."},"capacity":{"type":["integer","null"],"minimum":1,"maximum":100000,"description":"Most people who can say yes, counting whole parties. null = no cap."},"public_party_max":{"type":"integer","minimum":1,"maximum":10,"description":"People per RSVP on the public page, the person themselves included. Default 1."},"status":{"type":"string","enum":["draft","published","cancelled"],"description":"Invite links only work once the event is published. Allowed changes: draft → published; published → draft or cancelled; cancelled → published. New events can be draft (default) or published."},"public_url":{"type":["string","null"],"format":"uri","description":"The public event page, when visibility is public."},"cover_image_url":{"type":["string","null"],"format":"uri"},"web_url":{"type":"string","format":"uri","description":"The event in Fevori, for its host."},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"},"counts":{"type":"object","properties":{"guests":{"type":"integer"},"viewed":{"type":"integer","description":"Guests who have opened their invitation page."},"yes":{"type":"integer"},"maybe":{"type":"integer"},"no":{"type":"integer"},"attending":{"type":"integer","description":"People coming, counting whole parties."}},"required":["guests","viewed","yes","maybe","no","attending"]}},"required":["id","title","description","starts_at","ends_at","timezone","location_name","location_address","visibility","capacity","public_party_max","status","public_url","cover_image_url","web_url","created_at","updated_at","counts"]},"EventInput":{"type":"object","properties":{"title":{"type":"string","maxLength":200},"description":{"type":["string","null"],"maxLength":5000,"description":"What to bring, parking, dress code…"},"starts_at":{"type":["string","null"],"format":"date-time","description":"An instant with an offset, e.g. 2026-06-06T19:00:00-07:00. Returned in UTC."},"ends_at":{"type":["string","null"],"format":"date-time","description":"After starts_at; needs starts_at."},"timezone":{"type":"string","description":"IANA time zone the event happens in, used to show its times to guests. Default UTC.","examples":["America/Los_Angeles"]},"location_name":{"type":["string","null"],"maxLength":200},"location_address":{"type":["string","null"],"maxLength":500},"visibility":{"type":"string","enum":["private","public"],"description":"public: anyone can open the event page and RSVP, and it is listed for search engines. Default private."},"capacity":{"type":["integer","null"],"minimum":1,"maximum":100000,"description":"Most people who can say yes, counting whole parties. null = no cap."},"public_party_max":{"type":"integer","minimum":1,"maximum":10,"description":"People per RSVP on the public page, the person themselves included. Default 1."},"status":{"type":"string","enum":["draft","published","cancelled"],"description":"Invite links only work once the event is published. Allowed changes: draft → published; published → draft or cancelled; cancelled → published. New events can be draft (default) or published."}},"additionalProperties":false},"EventEnvelope":{"type":"object","properties":{"event":{"$ref":"#/components/schemas/Event"}},"required":["event"]},"GuestStatus":{"type":"string","enum":["unsent","sending","pending","opened","viewed","yes","maybe","no","bounced","failed","unsubscribed"],"description":"Where their invitation stands, furthest first across email and text: yes/maybe/no = replied; viewed = opened the invitation page; opened = opened the email; pending = sent; sending = queued; unsent = not sent yet; bounced/failed = could not be delivered; unsubscribed = they opted out of Fevori email."},"Guest":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string","maxLength":200},"email":{"type":["string","null"],"format":"email","maxLength":320,"description":"Unique within the event. Stored lowercase."},"phone":{"type":["string","null"],"maxLength":40,"description":"Stored in E.164 when it can be read (US numbers may omit +1)."},"max_party_size":{"type":"integer","minimum":1,"maximum":20,"description":"How many people this invitation covers, the guest included. Default 1."},"contact_id":{"type":["string","null"],"description":"The host’s contact this guest is linked to, if any."},"joined_via":{"type":"string","enum":["host","public_page"],"description":"Added by the host, or RSVP’d on the public event page."},"invite_url":{"type":"string","format":"uri","description":"Their personal invitation link. Works once the event is published."},"status":{"$ref":"#/components/schemas/GuestStatus"},"status_at":{"type":["string","null"],"format":"date-time","description":"When they reached that status, if known."},"rsvp":{"type":"object","properties":{"status":{"type":"string","enum":["pending","yes","no","maybe"]},"at":{"type":["string","null"],"format":"date-time"},"party_size":{"type":["integer","null"],"description":"How many are coming, them included."},"message":{"type":["string","null"],"description":"Note left with their reply."}},"required":["status","at","party_size","message"]},"viewed_at":{"type":["string","null"],"format":"date-time","description":"When they first opened their invitation page."},"created_at":{"type":"string","format":"date-time"}},"required":["id","name","email","phone","max_party_size","contact_id","joined_via","invite_url","status","status_at","rsvp","viewed_at","created_at"]},"GuestInput":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"email":{"type":["string","null"],"format":"email","maxLength":320,"description":"Unique within the event. Stored lowercase."},"phone":{"type":["string","null"],"maxLength":40,"description":"Stored in E.164 when it can be read (US numbers may omit +1)."},"max_party_size":{"type":"integer","minimum":1,"maximum":20,"description":"How many people this invitation covers, the guest included. Default 1."}},"additionalProperties":false},"GuestEnvelope":{"type":"object","properties":{"guest":{"$ref":"#/components/schemas/Guest"}},"required":["guest"]},"User":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":["string","null"],"format":"email","description":"null for an unclaimed agent account."},"claimed":{"type":"boolean","description":"false for an agent account no person has claimed yet."},"claim_url":{"type":["string","null"],"format":"uri","description":"Where a person claims this account, while unclaimed."},"expires_at":{"type":["string","null"],"format":"date-time","description":"When an unclaimed account is deleted."}},"required":["id","name","email","claimed","claim_url","expires_at"]},"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","examples":["unauthorized","not_found","invalid_json","invalid_request","email_taken","account_unclaimed","event_not_published","rate_limited","internal"]},"message":{"type":"string"},"details":{"type":"array","items":{"type":"string"},"description":"Each problem, for invalid_request."},"claim_url":{"type":"string","format":"uri","description":"For account_unclaimed: where a person claims the account."}},"required":["code","message"]}},"required":["error"]}}}}