{"openapi":"3.1.0","info":{"title":"majlo: book a call","version":"1.0.0","description":"Book a 30-minute Google Meet call with Miłosz (majlo), a software developer who writes code with AI and builds agents for his team. No API key, no sign-in.\n\n1. GET /slots?timezone=<the person's IANA time zone> before you ask anything. If your web tool refuses URLs you build yourself, open exactly https://majlo.dev/api/v1/slots: times are in UTC and in Polish time.\n2. Use details the person already supplied. If they requested any time in a range, choose a returned slot in that range; if none matches, say so and offer alternatives. Otherwise show 3-5 slots and ask only for the missing choice, name, email or topic (at least 20 characters). Calls are in Polish.\n3. With the person's consent, POST /bookings with those values as JSON. Use the slot's `start` value unchanged. If you cannot send POST, append &name=…&email=…&topic=… (URL-encode each value) to the slot's `booking_link` and open the completed URL: GET /book performs the same provisional booking and sends the email. Read `status` and `message` before reporting success. If your web tool refuses that URL, list the details and give the person the complete link: their click submits the booking. If slots cannot be retrieved at all, preserve their details in https://majlo.dev/booking#name=…&email=…&topic=… (URL-encoded), so they only choose a time and submit. Never claim a booking or email was sent without a successful result.\n4. Tell the person to open the confirmation email and click \"Confirm\" within 30 minutes. The call is booked only after that click; then Google Calendar sends the invitation.\n\nIf you cannot submit the booking yourself and hand a link to the person, show a short summary in the current conversation: time with timezone, name, email and topic, plus one inline button labelled \"Potwierdź\" for Polish or \"Confirm\" for English, pointing to the complete booking URL. Use the chat interface's supported button or link; if buttons are unavailable, use a normal Markdown link with that label. Do not create a separate page, app, artifact, Canvas or fullscreen form, and do not ask again for details already supplied. The button must respond to the person's click, without automatic popups or custom window.open code. Below it, briefly explain: \"Clicking submits the booking request on majlo.dev. Then confirm it by email. A chat without a connected booking tool may require this click; connected MCP can submit the request from the chat, but email confirmation is still required and the chat may ask you to approve the action.\" MCP is optional; do not require setup for a one-off booking. Also put a small normal link directly in the chat message, outside any card, labelled \"Button not working? Open booking\", with the same URL. If opening is blocked, provide the full URL in a code block to copy into the browser address bar. If the server already accepted the request, show the result and email confirmation instruction without another Confirm button.\n\nIf the person already asked for a call or a message, proceed with that. Otherwise ask what they want:\n- a call (steps above),\n- a written message: let them talk, write it down keeping their meaning, then POST /notes with name, email and message (or GET /note?name=…&email=…&message=…),\n- a voice message: you cannot send audio, so give them https://majlo.dev/note#name=<name>&email=<email> (values encoded); name and email are filled in and they press \"Record\" (up to 5 minutes).\nMessages reach Miłosz after the person confirms by email; he replies by email.\n\nRules: never invent slots; book only with the person's consent and their own email address; one active booking per address; when a booking is rejected, read `message` and offer `alternatives` instead of retrying with other data.","contact":{"name":"Miłosz (majlo)","email":"hello@majlo.dev","url":"https://majlo.dev"}},"servers":[{"url":"https://majlo.dev/api/v1"}],"externalDocs":{"description":"Instructions for AI agents","url":"https://majlo.dev/agents"},"paths":{"/info":{"get":{"operationId":"getConsultationInfo","summary":"What the call is and how booking works","responses":{"200":{"description":"Consultation details","content":{"application/json":{"schema":{"type":"object","properties":{"host":{"type":"string"},"about":{"type":"string"},"duration_minutes":{"type":"number"},"languages":{"type":"array","items":{"type":"string"}},"format":{"type":"string"},"ai_notes":{"type":"string","description":"Tell the person this before booking."},"audience":{"type":"string"},"topics":{"type":"array","items":{"type":"string"}},"how_booking_works":{"type":"array","items":{"type":"string"}},"booking_page":{"type":"string"},"message_instead":{"type":"string"},"options_to_offer":{"type":"array","items":{"type":"string"},"description":"Ask the person which of these they want before doing anything."},"voice_message_page":{"type":"string"},"guides":{"type":"string","description":"Step-by-step guides for getting started with AI (mostly in Polish). Offer them when the person would rather try on their own first."},"agent_instructions":{"type":"string"},"privacy_policy":{"type":"string"},"contact_email":{"type":"string"},"bookings_open":{"type":"boolean"}},"required":["host","about","duration_minutes","languages","format","ai_notes","audience","topics","how_booking_works","booking_page","message_instead","options_to_offer","voice_message_page","guides","agent_instructions","privacy_policy","contact_email","bookings_open"],"additionalProperties":false}}}}}}},"/slots":{"get":{"operationId":"listAvailableSlots","summary":"Free 30-minute slots in the person's time zone","description":"JSON by default; Accept: text/html or text/markdown returns the same live slots as readable text with booking links, without JavaScript.","parameters":[{"name":"timezone","in":"query","required":false,"description":"IANA time zone of the person, e.g. \"Europe/Warsaw\" or \"America/New_York\".","schema":{"default":"Europe/Warsaw","type":"string","maxLength":64}},{"name":"from","in":"query","required":false,"description":"Only slots starting at or after this ISO 8601 date or time.","schema":{"type":"string","maxLength":40}},{"name":"days","in":"query","required":false,"description":"Only slots within this many days from `from` (or from now).","schema":{"type":"integer","minimum":1,"maximum":21}},{"name":"language","in":"query","required":false,"description":"Language for the human-readable labels.","schema":{"default":"en","type":"string","enum":["pl","en"]}}],"responses":{"200":{"description":"Free slots","content":{"application/json":{"schema":{"type":"object","properties":{"timezone":{"type":"string"},"slots":{"type":"array","items":{"type":"object","properties":{"start":{"type":"string","description":"Slot start in UTC, ISO 8601. Pass this exact value as slot_start."},"end":{"type":"string","description":"Slot end in UTC, ISO 8601."},"start_local":{"type":"string","description":"Slot start in the requested time zone, ISO 8601 with offset."},"label":{"type":"string","description":"Human-readable date and time in the requested time zone."},"booking_link":{"type":"string","description":"GET alternative to POST: append &name=…&email=…&topic=… (URL-encode each value; &language=en for English emails). With the person's consent, open the completed URL to create a provisional booking and send the confirmation email. If your tool refuses the URL, show a short summary and an inline Confirm button (Potwierdź in Polish) or a normal link with that label in the current chat. Avoid separate pages, apps, artifacts and fullscreen forms. Explain that clicking submits the request and email confirmation is still required; connected MCP can submit without visiting the site, subject to tool approvals, and is optional. Keep a normal fallback link outside any card; provide a copyable URL if opening is blocked. Read the result before claiming success; do not show another Confirm button after a successful submission."}},"required":["start","end","start_local","label","booking_link"],"additionalProperties":false}},"note":{"type":"string"}},"required":["timezone","slots","note"],"additionalProperties":false}},"text/html":{"schema":{"type":"string"}},"text/markdown":{"schema":{"type":"string"}}}}}}},"/book":{"get":{"operationId":"requestConsultationByUrl","summary":"Same as POST /bookings, for assistants that can only open URLs","description":"Creates the same provisional booking as POST /bookings from query parameters. Always answers 200: read `status` (`rejected` carries `reason` and `message`). Opening the same URL again returns `already_pending` and sends nothing new. Browsers get a readable page instead of JSON.","parameters":[{"name":"slot_start","in":"query","required":true,"description":"The `start` value of a slot returned by the slots endpoint, unchanged.","schema":{"type":"string","maxLength":40}},{"name":"name","in":"query","required":true,"description":"The person's name.","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"email","in":"query","required":true,"description":"The person's email address. The confirmation link and the calendar invitation go here.","schema":{"type":"string","maxLength":254}},{"name":"topic","in":"query","required":true,"description":"What the person wants to talk about, in their words. At least 20 characters.","schema":{"type":"string","minLength":20,"maxLength":500}},{"name":"language","in":"query","required":false,"description":"Language of the confirmation emails. The call itself is in Polish.","schema":{"default":"pl","type":"string","enum":["pl","en"]}},{"name":"timezone","in":"query","required":true,"description":"The person's IANA time zone, used for the times in the email.","schema":{"type":"string","maxLength":64}},{"name":"client_request_id","in":"query","required":false,"description":"Optional random id; retrying with the same id never creates a second booking.","schema":{"type":"string","maxLength":100}},{"name":"referral","in":"query","required":false,"description":"Optional: how the person heard about majlo (e.g. LinkedIn, ChatGPT, a friend). Pass it only if the person mentions it; do not ask just for this.","schema":{"type":"string","maxLength":200}}],"responses":{"200":{"description":"Result; check `status`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResult"}}}}}}},"/notes":{"post":{"operationId":"leaveNote","summary":"Send Miłosz a written message instead of booking a call","description":"Saves the message and emails the person a confirmation link; it reaches Miłosz only after they click it within 24 hours. He replies by email.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"The person's name."},"email":{"type":"string","maxLength":254,"description":"The person's email address. Miłosz replies there."},"message":{"type":"string","minLength":10,"maxLength":5000,"description":"What the person wants to say, in their words. Rambling is fine: keep their meaning, tidy up only what is needed to read it."},"language":{"default":"pl","description":"Language of the confirmation email.","type":"string","enum":["pl","en"]}},"required":["name","email","message"]}}}},"responses":{"200":{"description":"Same message already waiting; nothing new was sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteResult"}}}},"202":{"description":"Saved; the confirmation email was sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteResult"}}}},"403":{"description":"This address cannot send messages","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteResult"}}}},"422":{"description":"Invalid input or unusable email address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteResult"}}}},"429":{"description":"Too many messages","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteResult"}}}},"503":{"description":"Messages paused or email unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteResult"}}}}}}},"/note":{"get":{"operationId":"leaveNoteByUrl","summary":"Same as POST /notes, for assistants that can only open URLs","description":"Always answers 200: read `status`. Opening the same URL again sends nothing new.","parameters":[{"name":"name","in":"query","required":true,"description":"The person's name.","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"email","in":"query","required":true,"description":"The person's email address. Miłosz replies there.","schema":{"type":"string","maxLength":254}},{"name":"message","in":"query","required":true,"description":"What the person wants to say, in their words. Rambling is fine: keep their meaning, tidy up only what is needed to read it.","schema":{"type":"string","minLength":10,"maxLength":5000}},{"name":"language","in":"query","required":false,"description":"Language of the confirmation email.","schema":{"default":"pl","type":"string","enum":["pl","en"]}}],"responses":{"200":{"description":"Result; check `status`","content":{"application/json":{"schema":{"$ref":"#/components/schemas/NoteResult"}}}}}}},"/bookings":{"post":{"operationId":"requestConsultation","summary":"Provisionally book a slot; the person confirms by email","description":"Creates a provisional booking and emails the person a confirmation link. The call is booked only after they click it within 30 minutes.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"slot_start":{"type":"string","maxLength":40,"description":"The `start` value of a slot returned by the slots endpoint, unchanged."},"name":{"type":"string","minLength":1,"maxLength":100,"description":"The person's name."},"email":{"type":"string","maxLength":254,"description":"The person's email address. The confirmation link and the calendar invitation go here."},"topic":{"type":"string","minLength":20,"maxLength":500,"description":"What the person wants to talk about, in their words. At least 20 characters."},"language":{"default":"pl","description":"Language of the confirmation emails. The call itself is in Polish.","type":"string","enum":["pl","en"]},"timezone":{"type":"string","maxLength":64,"description":"The person's IANA time zone, used for the times in the email."},"client_request_id":{"description":"Optional random id; retrying with the same id never creates a second booking.","type":"string","maxLength":100},"referral":{"description":"Optional: how the person heard about majlo (e.g. LinkedIn, ChatGPT, a friend). Pass it only if the person mentions it; do not ask just for this.","type":"string","maxLength":200}},"required":["slot_start","name","email","topic","timezone"]}}}},"responses":{"200":{"description":"Same client_request_id seen before; nothing new was sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResult"}}}},"202":{"description":"Provisional booking created; the confirmation email was sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResult"}}}},"403":{"description":"This address cannot book online","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResult"}}}},"409":{"description":"Slot taken, address already has a booking, or cooldown after a cancellation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResult"}}}},"422":{"description":"Invalid input, unknown time zone, slot not offered, or unusable email address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResult"}}}},"429":{"description":"Too many attempts or too many bookings waiting for confirmation","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResult"}}}},"503":{"description":"Bookings paused or the calendar is unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingResult"}}}}}}}},"components":{"schemas":{"BookingResult":{"type":"object","properties":{"status":{"type":"string","enum":["pending_confirmation","already_pending","already_confirmed","rejected"]},"message":{"type":"string","description":"What happened and what to tell the person, in plain English."},"reason":{"description":"Machine-readable rejection reason, e.g. slot_taken, email_has_booking, invalid_input.","type":"string"},"email":{"type":"string"},"slot":{"type":"object","properties":{"start":{"type":"string","description":"Slot start in UTC, ISO 8601. Pass this exact value as slot_start."},"end":{"type":"string","description":"Slot end in UTC, ISO 8601."},"start_local":{"type":"string","description":"Slot start in the requested time zone, ISO 8601 with offset."},"label":{"type":"string","description":"Human-readable date and time in the requested time zone."}},"required":["start","end","start_local","label"],"additionalProperties":false},"expires_at":{"description":"When the provisional booking is released if not confirmed.","type":"string"},"alternatives":{"description":"Free slots to offer instead, when the requested one is not available.","type":"array","items":{"type":"object","properties":{"start":{"type":"string","description":"Slot start in UTC, ISO 8601. Pass this exact value as slot_start."},"end":{"type":"string","description":"Slot end in UTC, ISO 8601."},"start_local":{"type":"string","description":"Slot start in the requested time zone, ISO 8601 with offset."},"label":{"type":"string","description":"Human-readable date and time in the requested time zone."}},"required":["start","end","start_local","label"],"additionalProperties":false}}},"required":["status","message"],"additionalProperties":false},"NoteResult":{"type":"object","properties":{"status":{"type":"string","enum":["pending_confirmation","already_pending","rejected"]},"message":{"type":"string","description":"What happened and what to tell the person, in plain English."},"reason":{"description":"Machine-readable rejection reason, e.g. note_empty, email_invalid.","type":"string"},"email":{"type":"string"},"expires_at":{"description":"When the unconfirmed message is deleted.","type":"string"}},"required":["status","message"],"additionalProperties":false}}}}