nextmail.calendar
Every method in this namespace: its signature, its parameters, what it returns and an example.
Methods
Calendar events found in mail, expanded into occurrences.
calendar.list_events()calendar.list_all_events()calendar.iterate_events()calendar.get_event()calendar.get_event_ics()calendar.create_event()calendar.update_event()calendar.delete_event()calendar.cancel_event()calendar.respond_to_event()calendar.decline_proposal()calendar.hide_event()calendar.unhide_event()calendar.free_busy()calendar.list_feeds()calendar.create_feed()calendar.reset_feed()calendar.delete_feed()calendar.list_subscriptions()calendar.create_subscription()calendar.update_subscription()calendar.refresh_subscription()calendar.delete_subscription()calendar.import_ics()calendar.list_imports()calendar.list_all_imports()calendar.iterate_imports()calendar.get_import()calendar.undo_import()
calendar.list_events()
List one page of event occurrences inside a time window
def list_events( *, limit: int | None = None, cursor: str | None = None, from_: datetime | str, to: datetime | str, timezone: str | None = None, include_team: bool | None = None, q: str | None = None, api_key: str | None = None, timeout: float | None = None,) -> Page[CalendarOccurrenceResource]Expands your events into their occurrences between from_= and to= and returns one page of them, sorted by start time and then by event id. A recurring series gives one row per instance in the window, with its excluded dates left out, and every row points back to its event through eventId. The window is required because a repeating series has no end to list up to. Follow nextCursor while hasMore is True, with the same from_=, to= and timezone=, to read every occurrence, or let list_all_events and iterate_events do that walk.
to= must be after from_= and at most 366 days later. A missing bound is a 422 invalid_parameter, while a value that does not parse, a reversed or overlong window, or an unknown zone is a 400 invalid_parameter naming the field. Both bounds take a datetime or an ISO 8601 string. The SDK sends a datetime as a UTC instant, and reads a naive one as local time.
timezone= is an IANA zone and defaults to UTC on the server. It is used to expand any event stored without a zone of its own, which is what decides the day an all day event lands on.
Parameters
limitintOccurrences per page, a whole number from 1 to 100. The server defaults to 25.
cursorstrThe
nextCursorof the previous page, sent with the same window. It is opaque, so never build one yourself.from_datetime | strRequiredStart of the window, a
datetimeor an ISO 8601 string, sent as thefromquery parameter.todatetime | strRequiredEnd of the window, after
from_=and at most 366 days later.timezonestrIANA zone such as
Europe/London, at most 64 characters. Defaults toUTC.include_teamboolTrueadds the events of the rest of the workspace, apart from the ones marked private. Only a key of the workspace owner sees more with it. Defaults toFalse.qstrWords to look for in the title, the place and the organiser, at most 200 characters. Only events that hold every word come back, so pass a long window to find an event by name.
api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
Page[CalendarOccurrenceResource], a dict with items, hasMore and nextCursor. Each item has eventId, uid, start, end, allDay, summary, location, status, transparency, organizerEmail, recurring, myPartstat and attendeeCount, and occurrence['isOrganizer'] is True when you organise the event.
Example
from datetime import datetime, timedelta, timezone from nextmail import nextmail start = datetime.now(timezone.utc)page = nextmail.calendar.list_events( from_=start, to=start + timedelta(days=7), timezone='Europe/London', limit=50) for occurrence in page['items']: print(occurrence['start'], occurrence['summary'], occurrence['myPartstat'] or 'not invited') print(page['hasMore'], page['nextCursor'])Notes
The list holds your own events: the ones you made and the ones an address of yours organises or attends. For the workspace owner and their keys, an address counts as theirs unless it was given to a teammate, and
include_team=Trueadds the rest of the workspace, apart from events somebody marked private. A member's app and a narrowed key always get their own view.myPartstatis the response recorded for one of your own addresses, or for the addresses a narrowed key covers, andNonewhen none of them is an attendee.A narrowed key only sees occurrences whose organiser is one of the addresses it covers or where one of them is an attendee.
Cancelled events are not filtered out. Check
statusforCANCELLED.Nothing in the window is dropped. A series repeats at most once a day, so one series gives at most one row per day of the window, and every one of them is reachable through the pages.
The cursor holds the start time and event id of the last row, so an event deleted or moved between pages never breaks the walk. A cursor this list did not hand out is a 400
invalid_cursor.
Also available in
calendar.list_all_events()
Collect every occurrence inside a time window into one list
def list_all_events( *, limit: int | None = None, cursor: str | None = None, from_: datetime | str, to: datetime | str, timezone: str | None = None, include_team: bool | None = None, q: str | None = None, api_key: str | None = None, timeout: float | None = None,) -> builtins.list[CalendarOccurrenceResource]Walks every page of list_events for one window and returns all of its occurrences in one list, sorted by start time and then by event id. It follows nextCursor until hasMore is False, sending the same from_=, to= and timezone= with each request.
A window of up to 366 days of a busy calendar can hold thousands of occurrences, so prefer iterate_events when you can stop early.
Parameters
limitintPage size for each request, 1 to 100. The server defaults to 25.
cursorstrStarts the walk from this cursor instead of the start of the window.
from_datetime | strRequiredStart of the window, a
datetimeor an ISO 8601 string, sent as thefromquery parameter.todatetime | strRequiredEnd of the window, after
from_=and at most 366 days later.timezonestrIANA zone such as
Europe/London, at most 64 characters. Defaults toUTC.include_teamboolTrueadds the events of the rest of the workspace, apart from the ones marked private. Only a key of the workspace owner sees more with it. Defaults toFalse.qstrWords to look for in the title, the place and the organiser, at most 200 characters. Only events that hold every word come back, so pass a long window to find an event by name.
api_keystrOverrides the client's API key for every page of this walk.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
list[CalendarOccurrenceResource] holding every occurrence in the window, earliest first.
Example
from nextmail import nextmail occurrences = nextmail.calendar.list_all_events( from_='2026-10-01T00:00:00Z', to='2026-11-01T00:00:00Z', limit=100) busy = [occurrence for occurrence in occurrences if occurrence['transparency'] == 'OPAQUE']print(len(busy), 'busy blocks in October')Notes
If any page fails the call raises, and the occurrences already fetched are discarded.
Also available in
calendar.iterate_events()
Stream the occurrences inside a time window one at a time
def iterate_events( *, limit: int | None = None, cursor: str | None = None, from_: datetime | str, to: datetime | str, timezone: str | None = None, include_team: bool | None = None, q: str | None = None, api_key: str | None = None, timeout: float | None = None,) -> Iterator[CalendarOccurrenceResource]Returns a generator over the occurrences in one window that yields them one at a time, earliest first, and fetches the next page only when the current one is used up. Nothing is requested until the loop starts, and leaving the loop with break stops further requests, which makes it the way to find the next free slot or the next meeting without reading the whole window.
The walk ends when hasMore is False, when a page has no nextCursor, or when the server repeats a cursor.
Parameters
limitintPage size per request, 1 to 100. The server defaults to 25.
cursorstrStarts the walk from this cursor instead of the start of the window.
from_datetime | strRequiredStart of the window, a
datetimeor an ISO 8601 string, sent as thefromquery parameter.todatetime | strRequiredEnd of the window, after
from_=and at most 366 days later.timezonestrIANA zone such as
Europe/London, at most 64 characters. Defaults toUTC.include_teamboolTrueadds the events of the rest of the workspace, apart from the ones marked private. Only a key of the workspace owner sees more with it. Defaults toFalse.qstrWords to look for in the title, the place and the organiser, at most 200 characters. Only events that hold every word come back, so pass a long window to find an event by name.
api_keystrOverrides the client's API key for every page of this walk.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
Iterator[CalendarOccurrenceResource], a generator yielding one occurrence per step.
Example
from datetime import datetime, timedelta, timezone from nextmail import nextmail start = datetime.now(timezone.utc) for occurrence in nextmail.calendar.iterate_events(from_=start, to=start + timedelta(days=30)): if occurrence['myPartstat'] is None: continue print('Next meeting:', occurrence['start'], occurrence['summary']) breakNotes
timeout=bounds each page request on its own, not the whole walk. A page that fails raises out of theforloop, after the occurrences already yielded.
Also available in
calendar.get_event()
Read an event with its attendees
def get_event( id: str, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarEventResourceReturns a stored event with its attendees and their responses. What the server understood from the iCalendar data is structured here: recurrence holds the RRULE text, exdates the excluded instances, organizer the organiser and alarms the reminders, each a dict with minutesBefore and action. The raw iCalendar source is not included, and get_event_ics serves the event as a document.
id is the event id, the eventId on an occurrence, and not the iCalendar uid. A recurring series is one event however many occurrences it has, so start and end describe its first instance. messageId and threadId link the event to mail in the mailbox when there is any.
Attendees come back in the order they were added, each with partstat, role, rsvp, cutype and respondedAt. A narrowed key gets a 404 unless one of the addresses it covers organises the event or is on the attendee list, and a key that holds a whole domain covers every address on it.
Parameters
idstrRequiredEvent id, the
eventIdof an occurrence, such ascal_followed by 24 hex characters.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarEventResource with id, uid, sequence, summary, description, location, url, start, end, allDay, timezone, recurrence, exdates, status, transparency, visibility, organizer, source, messageId, threadId, alarms, attendees, createdAt and updatedAt. event['isOrganizer'] is True when you organise it, and so may change it.
Example
from nextmail import nextmail event = nextmail.calendar.get_event('cal_3f9a1c7e5b2d48a06c1e9f4b') print(event['summary'], event['recurrence'] or 'one off') for attendee in event['attendees']: print(attendee['email'], attendee['partstat'])Notes
status,transparencyandvisibilitycarry iCalendar values, and default toCONFIRMED,OPAQUEandPUBLIC.An attendee's
partstatdefaults toNEEDS-ACTIONandroletoREQ-PARTICIPANTuntil a response is recorded.
Also available in
calendar.get_event_ics()
Download an event as an iCalendar document
def get_event_ics( id: str, *, api_key: str | None = None, timeout: float | None = None,) -> strReturns the event serialised as an .ics document, as a str rather than parsed JSON. The server sends it as text/calendar; charset=utf-8 with a download filename built from the event's uid, and the SDK asks for that type and returns the body as text.
The document declares METHOD:PUBLISH, and the content type carries no method parameter. That is deliberate: a REQUEST document makes a calendar client offer accept and decline and reply to the organiser, and this endpoint has no authority to invite anyone. Import it to show what the event currently is, not to send an invitation.
It holds one VEVENT with the attendees and their participation status, the recurrence rule, excluded dates and alarms. Lines end in CRLF and long lines are folded. DTSTAMP is the time of the request, so two downloads of an unchanged event differ on that line.
Parameters
idstrRequiredEvent id, the
eventIdof an occurrence.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
str, the iCalendar text from BEGIN:VCALENDAR to END:VCALENDAR.
Example
from pathlib import Path from nextmail import nextmail ics = nextmail.calendar.get_event_ics('cal_8d3f0a2b9c4e41f7a6b5c2d1')Path('planning.ics').write_text(ics, newline='') summary = next((line for line in ics.splitlines() if line.startswith('SUMMARY:')), None)print(summary)Notes
Errors still come back as JSON and raise
NextmailApiError, so a failure never comes back as a string.Timed events are written in the zone they were created in, as local times with a
TZIDand aVTIMEZONEthat carries the rules of that zone, and all day events asDATEvalues.Save it with
newline='', as inPath('event.ics').write_text(ics, newline=''), so the CRLF line endings are written as they are on every platform.A narrowed key gets a 404 on the same terms as
get_event.
Also available in
calendar.create_event()
Put an event on the calendar
def create_event( body: CalendarEventCreate, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarEventResourceCreates an event, as New event on the Calendar page of the app does. start and end take a datetime or an ISO 8601 string with a zone, and timezone is the IANA zone the event belongs to, which decides what an all-day event means. The SDK sends a datetime as a UTC instant, and reads a naive one as local time.
With attendees, an invitation goes to each of them by email from from, which has to be an address the key may send as, so the call also needs emails:send. Send 'sendInvites': False to keep it to your own calendar.
Parameters
body['summary']strRequiredThe title, 1 to 255 characters.
body['start']datetime | strRequiredWhen it starts, a
datetimeor an ISO 8601 string with a zone.body['end']datetime | strRequiredWhen it ends, after
startand at most two years later.body['description']strNotes about the event, up to 8,000 characters.
body['location']strWhere it is: a room, an address or a link.
body['url']strA link that belongs to the event.
body['meetingUrl']strThe link to the video call, such as a Zoom, Meet or Teams address. The invitation carries it, so guests get a Join button in their calendar too.
body['allDay']boolWhether it fills whole days.
startandendthen mark the days.body['timezone']strThe IANA zone the event belongs to,
UTCunless you say.body['recurrence']strAn RRULE such as
FREQ=WEEKLY;BYDAY=MOwhen it repeats.body['attendees']list[CalendarAttendeeInput]Up to 200 people to invite, each a dict with
emailand optionallynameandoptional, such as{'email': '[email protected]', 'name': 'Ada Lovelace'}.body['reminders']list[CalendarReminderInput]Up to five reminders, each a dict with
minutesBeforeand optionallyaction, one ofDISPLAY,EMAILandAUDIO, such as{'minutesBefore': 15}.body['transparency']CalendarTransparencyOPAQUEshows you busy andTRANSPARENTfree.body['visibility']CalendarVisibilityPUBLIC,PRIVATEorCONFIDENTIAL.body['sendInvites']boolFalsekeeps the event to your own calendar and sends nothing.body['from']strThe address the invitations come from. It has to be one the key may send as.
api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarEventResource for the new event, with its attendees and their responses.
Example
from datetime import datetime, timedelta, timezone from nextmail import nextmail start = datetime(2026, 10, 12, 9, 0, tzinfo=timezone.utc)event = nextmail.calendar.create_event( { 'summary': 'Planning', 'start': start, 'end': start + timedelta(minutes=30), 'attendees': [{'email': '[email protected]', 'name': 'Ada Lovelace'}], 'reminders': [{'minutesBefore': 15}], 'from': '[email protected]', }) print(event['id'], event['start'], len(event['attendees']))Notes
The SDK does not retry it, because a second call makes a second event and invites everybody again.
A key that may not send as
fromis refused with 403from_address_forbidden, and an event with attendees and nofromwith 422invalid_parameter. Invitations withoutemails:sendare a 403insufficient_scope, whichis_scope_missingon the error reports.
Also available in
calendar.update_event()
Change an event
def update_event( id: str, patch: CalendarEventPatch, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarEventResourceChanges an event you organise. A field you leave out keeps its value, and attendees replaces the whole list. Everyone invited gets the updated invitation by email from from, so a change to an event with attendees needs emails:send too, unless sendInvites is False.
For a repeating event, scope says which dates change, as the app asks when you edit one date: occurrence changes only the date that starts at occurrenceStart, following that date and every one after it, and series all of them. With following the later dates become a new series with its own id, which is what comes back, and the old one ends the day before.
Parameters
idstrRequiredEvent id, the
eventIdof an occurrence, such ascal_followed by 24 hex characters.patch['summary']strThe title, 1 to 255 characters.
patch['start']datetime | strWhen it starts, a
datetimeor an ISO 8601 string with a zone.patch['end']datetime | strWhen it ends, after
startand at most two years later.patch['description']strNotes about the event, up to 8,000 characters.
patch['location']strWhere it is: a room, an address or a link.
patch['url']strA link that belongs to the event.
patch['meetingUrl']strThe link to the video call. An empty string takes it off.
patch['allDay']boolWhether it fills whole days.
startandendthen mark the days.patch['timezone']strThe IANA zone the event belongs to,
UTCunless you say.patch['recurrence']strAn RRULE such as
FREQ=WEEKLY;BYDAY=MOwhen it repeats.patch['attendees']list[CalendarAttendeeInput]Up to 200 people to invite, each a dict with
emailand optionallynameandoptional. It replaces the whole list.patch['reminders']list[CalendarReminderInput]Up to five reminders, each a dict with
minutesBeforeand optionallyaction, one ofDISPLAY,EMAILandAUDIO.patch['transparency']CalendarTransparencyOPAQUEshows you busy andTRANSPARENTfree.patch['visibility']CalendarVisibilityPUBLIC,PRIVATEorCONFIDENTIAL.patch['sendInvites']boolFalsekeeps the change to your own calendar and sends nothing.patch['from']strThe address the invitations come from. It has to be one the key may send as.
patch['scope']CalendarEditScopeoccurrence,followingorseries: which dates of a repeating event change. Left out, the change reaches the event or date the id names.patch['occurrenceStart']datetime | strThe start of the date you mean, a
datetimeor an ISO 8601 string aslist_eventsgives it. Needed withoccurrenceandfollowing.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarEventResource as it stands after the change, with sequence one higher.
Example
from nextmail import nextmail event = nextmail.calendar.update_event( 'cal_3f9a1c7e5b2d48a06c1e9f4b', {'start': '2026-10-12T10:00:00Z', 'end': '2026-10-12T10:30:00Z'},) print(event['start'], event['sequence'])Notes
The SDK does not retry it, because a retry after a lost response sends the update to everybody again.
An event organised by somebody else is refused with 403
not_organizer: answer it withrespond_to_eventinstead. An event the key does not reach is a 404.
Also available in
calendar.delete_event()
Remove an event from the calendar
def delete_event( id: str, *, scope: CalendarEditScope | None = None, occurrence_start: datetime | str | None = None, api_key: str | None = None, timeout: float | None = None,) -> DeletedCalendarEventResourceRemoves an event for good, as Remove does in the app. Nobody else is told, so call off a meeting with others in it with cancel_event instead. For a repeating event, scope='occurrence' with occurrence_start= removes one date, and scope='following' that date and every one after it.
Parameters
idstrRequiredEvent id, the
eventIdof an occurrence, such ascal_followed by 24 hex characters.scopeCalendarEditScopeoccurrence,followingorseries: which dates of a repeating event go.occurrence_startdatetime | strThe start of the date you mean, a
datetimeor an ISO 8601 string aslist_eventsgives it. Needed withoccurrenceandfollowing.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
DeletedCalendarEventResource, such as {'object': 'calendar_event', 'id': 'cal_3f9a1c7e5b2d48a06c1e9f4b', 'deleted': True}.
Example
from nextmail import nextmail deleted = nextmail.calendar.delete_event('cal_3f9a1c7e5b2d48a06c1e9f4b') print(deleted['id'], deleted['deleted'])Notes
An event organised by somebody else is refused with 403
not_organizer, and an event that is gone or that the key does not reach is a 404. An event you may not remove can still be taken off your own calendar withhide_event.
Also available in
calendar.cancel_event()
Call off a meeting
def cancel_event( id: str, body: CalendarEventCancel | None = None, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarEventResourceCancels a meeting you organise, as Cancel meeting does in the app. It stays on the calendar with status CANCELLED, and everyone invited is told it is off by email from from, so a meeting with attendees also needs emails:send. body is optional.
For a repeating meeting, scope occurrence with occurrenceStart calls off one date, and following that date and every one after it. Guests get the matching update.
Parameters
idstrRequiredEvent id, the
eventIdof an occurrence, such ascal_followed by 24 hex characters.body['from']strThe address the cancellation comes from. It has to be one the key may send as.
body['scope']CalendarEditScopeoccurrence,followingorseries: which dates of a repeating meeting are called off.body['occurrenceStart']datetime | strThe start of the date you mean, a
datetimeor an ISO 8601 string aslist_eventsgives it. Needed withoccurrenceandfollowing.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarEventResource with status CANCELLED.
Example
from nextmail import nextmail event = nextmail.calendar.cancel_event( 'cal_3f9a1c7e5b2d48a06c1e9f4b', {'from': '[email protected]'}) print(event['summary'], event['status'])Notes
The SDK does not retry it, because a retry sends the cancellation again.
An event organised by somebody else is refused with 403
not_organizer.
Also available in
calendar.respond_to_event()
Answer an invitation
def respond_to_event( id: str, body: CalendarEventAnswer, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarEventResourceAccepts, declines or tentatively accepts an invitation, as the buttons of an invitation do in the app. The answer goes to the organiser by email from the invited address the key may send as, or from respondingAs when the invitation went to more than one of yours, so it needs emails:send as well.
Parameters
idstrRequiredEvent id, the
eventIdof an occurrence, such ascal_followed by 24 hex characters.body['response']CalendarResponseRequiredACCEPTED,DECLINEDorTENTATIVE.body['respondingAs']strWhich of your invited addresses answers, when there is more than one.
body['proposedStart']datetime | strA new start to propose with your answer, a
datetimeor an ISO 8601 string, sent together withproposedEnd.body['proposedEnd']datetime | strThe end of the proposed time.
api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarEventResource with your answer on your attendee row.
Example
from nextmail import nextmail event = nextmail.calendar.respond_to_event( 'cal_3f9a1c7e5b2d48a06c1e9f4b', {'response': 'ACCEPTED'}) for attendee in event['attendees']: print(attendee['email'], attendee['partstat'])Notes
An event that invited none of the addresses the key may send as is refused with 403
not_invited, and a cancelled meeting with 409event_cancelled.When the organiser cannot be reached, nothing changes and the call is a 409
reply_not_sent.With
proposedStartandproposedEndthe organiser gets a counter-proposal, as Propose a new time sends from Google Calendar and Outlook, and decides whether to move the meeting.
Also available in
calendar.decline_proposal()
Decline a proposed time
def decline_proposal( id: str, body: CalendarProposalDecline, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarEventResourceKeeps the original time of a meeting you organise after a guest proposed another one, as Keep the original does on the event in the app. The guest is told by email from from, or from the organising address, so it needs emails:send as well. To take the proposed time instead, change the event with update_event.
Parameters
idstrRequiredEvent id, the
eventIdof an occurrence, such ascal_followed by 24 hex characters.body['attendee']strRequiredThe guest whose proposal you decline, by email address.
body['from']strThe address the answer is sent from, when it is not the organising address.
api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarEventResource with the guest's proposedStart and proposedEnd cleared.
Example
from nextmail import nextmail event = nextmail.calendar.decline_proposal( 'cal_3f9a1c7e5b2d48a06c1e9f4b', {'attendee': '[email protected]'}) print(event['summary'], 'keeps its time')Notes
A guest who has not proposed a new time is refused with 404
proposal_not_found, and an event somebody else organises with 403not_organizer.
Also available in
calendar.hide_event()
Hide an event from your calendar
def hide_event( id: str, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarEventHiddenResourceTakes an event off your own calendar and leaves it for everyone else, as Hide from my calendar does in the app. Use it for an invitation that reached you through a shared address, or an event you may not remove. It hides every date of a repeating event, and stops its reminders for you. unhide_event brings it back.
Parameters
idstrRequiredEvent id, the
eventIdof an occurrence, such ascal_followed by 24 hex characters.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarEventHiddenResource, such as {'object': 'calendar_event', 'id': 'cal_3f9a1c7e5b2d48a06c1e9f4b', 'uid': '...', 'hidden': True}.
Example
from nextmail import nextmail hidden = nextmail.calendar.hide_event('cal_3f9a1c7e5b2d48a06c1e9f4b') print(hidden['id'], hidden['hidden'])Notes
Hiding an event that is already hidden changes nothing, so it is safe to repeat. An event the key does not reach is a 404.
It hides the event for the person the key or app acts for, and nobody else.
list_eventsleaves it out from then on, andget_eventstill finds it, withhiddenTrue.
Also available in
calendar.unhide_event()
Show a hidden event again
def unhide_event( id: str, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarEventHiddenResourcePuts an event you hid back on your calendar, as Show on my calendar does on the invitation in the app.
Parameters
idstrRequiredEvent id, the
eventIdof an occurrence, such ascal_followed by 24 hex characters.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarEventHiddenResource with hidden False.
Example
from nextmail import nextmail shown = nextmail.calendar.unhide_event('cal_3f9a1c7e5b2d48a06c1e9f4b') print(shown['id'], shown['hidden'])Notes
An event that was not hidden stays as it was, so it is safe to repeat.
Also available in
calendar.free_busy()
Free and busy times
def free_busy( *, emails: Sequence[str], from_: datetime | str, to: datetime | str, timezone: str | None = None, duration_minutes: int | None = None, api_key: str | None = None, timeout: float | None = None,) -> CalendarFreeBusyResourceWhen the people you name are busy, as Find a time in the event editor of the app shows it. An address of the workspace shows the busy times of the person who holds it, from their events, the invitations they have not declined, their subscribed calendars and their bookings, without any detail. Any other address comes back with known False.
With duration_minutes=, suggestions lists up to five times when everyone known is free, on weekdays between 09:00 and 17:00 in timezone=. from_= and to= take a datetime or an ISO 8601 string and may be at most 31 days apart.
Parameters
emailsSequence[str]RequiredThe addresses to look up, at most 20.
from_datetime | strRequiredThe start of the window, a
datetimeor an ISO 8601 string, sent as thefromquery parameter.todatetime | strRequiredThe end of the window, at most 31 days after
from_=.timezonestrThe IANA zone the suggested times keep to working hours in,
UTCunless you say.duration_minutesintThe length of the meeting, 5 to 480 minutes. Send it to get
suggestions.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarFreeBusyResource with people, each a dict with email, known and busy, its busy times as dicts with start and end, and suggestions in the same shape.
Example
from datetime import datetime, timedelta, timezone from nextmail import nextmail start = datetime.now(timezone.utc)times = nextmail.calendar.free_busy( emails=['[email protected]', '[email protected]'], from_=start, to=start + timedelta(days=7), timezone='Europe/London', duration_minutes=30,) for person in times['people']: print(person['email'], len(person['busy']), 'busy blocks') for suggestion in times['suggestions']: print('Free:', suggestion['start'])Notes
A key or an app limited to particular addresses or domains is refused with 422
capability_unsupported, because the answer reads whole calendars.It reads the calendars as they are now and holds nothing, so a suggested time can be gone a moment later.
Also available in
calendar.list_feeds()
List calendar links
def list_feeds( *, api_key: str | None = None, timeout: float | None = None,) -> builtins.list[CalendarFeedResource]Returns the secret links that publish your calendar to other calendar apps, as the Share tab of the calendar settings in the app lists them. Anyone with a link can read what it shows, so treat url and webcalUrl like passwords.
Parameters
api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
list[CalendarFeedResource], each with id, name, mode, url, webcalUrl and when calendar apps last read it.
Example
from nextmail import nextmail for feed in nextmail.calendar.list_feeds(): print(feed['name'], feed['mode'], feed['url'])Notes
A key limited to particular addresses or domains is refused with 422
capability_unsupported, because a link covers the whole calendar.
Also available in
calendar.create_feed()
Create a calendar link
def create_feed( body: CalendarFeedCreate, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarFeedResourceMakes a secret link that publishes your calendar as an iCalendar feed, for Google Calendar (From URL), Outlook (Subscribe from web) or Apple Calendar (New subscription). full shows every detail, busy only the times you are busy. The calendar app reads the link again on its own schedule, usually every few hours.
Parameters
body['name']strRequiredThe name calendar apps show for the calendar, up to 100 characters.
body['mode']CalendarFeedModeRequiredfullfor every detail,busyfor busy times only.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarFeedResource with the new url and webcalUrl.
Example
from nextmail import nextmail feed = nextmail.calendar.create_feed({'name': 'Work (busy)', 'mode': 'busy'}) print('Paste into Google Calendar:', feed['url'])Notes
One person may keep 10 links. One more is refused with 409
calendar_feed_limit.
Also available in
calendar.reset_feed()
Reset a calendar link
def reset_feed( id: str, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarFeedResourceReplaces the secret in a calendar link, so the old address stops working everywhere it was shared. Paste the new address into the calendar apps you still want to update.
Parameters
idstrRequiredCalendar link id,
cfd_followed by 24 hex characters.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarFeedResource with its new url and webcalUrl.
Example
from nextmail import nextmail feed = nextmail.calendar.reset_feed('cfd_3f9a1c7e5b2d48a06c1e9f4b') print(feed['url'])Notes
A link that is not yours is a 404
calendar_feed_not_found.
Also available in
calendar.delete_feed()
Delete a calendar link
def delete_feed( id: str, *, api_key: str | None = None, timeout: float | None = None,) -> DeletedCalendarFeedResourceTurns a calendar link off for good. Calendar apps that subscribed to it stop getting updates.
Parameters
idstrRequiredCalendar link id,
cfd_followed by 24 hex characters.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
DeletedCalendarFeedResource with deleted set to True.
Example
from nextmail import nextmail deleted = nextmail.calendar.delete_feed('cfd_3f9a1c7e5b2d48a06c1e9f4b') print(deleted['id'], deleted['deleted'])Notes
A link that is not yours is a 404
calendar_feed_not_found.
Also available in
calendar.list_subscriptions()
List calendar subscriptions
def list_subscriptions( *, api_key: str | None = None, timeout: float | None = None,) -> builtins.list[CalendarSubscriptionResource]Returns the other calendars you subscribe to by link, such as a Google or Outlook calendar address, a team rota or public holidays, as Other calendars in the app lists them. Their events show on your calendar, read-only.
Parameters
api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
list[CalendarSubscriptionResource], each with name, url, color, status, lastError and how many events it holds.
Example
from nextmail import nextmail for subscription in nextmail.calendar.list_subscriptions(): print(subscription['name'], subscription['status'], subscription['eventCount'])Notes
statusisfailingwhile reads fail and are retried, andstoppedafter ten failures in a row until you refresh it.
Also available in
calendar.create_subscription()
Subscribe to a calendar
def create_subscription( body: CalendarSubscriptionCreate, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarSubscriptionResourceAdds a calendar by its iCalendar link: an https, http or webcal address, such as the secret address of a Google calendar or a published Outlook calendar. It is read at once, then again about every half hour. name defaults to the name the calendar gives itself.
Parameters
body['url']strRequiredThe calendar link,
https,httporwebcal.body['name']strThe name to show, up to 100 characters.
body['color']CalendarColorThe colour of its events, from
CALENDAR_COLORS.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarSubscriptionResource with its first events read.
Example
from nextmail import nextmail holidays = nextmail.calendar.create_subscription( {'url': 'webcal://example.com/holidays.ics', 'color': 'green'}) print(holidays['name'], holidays['eventCount'])Notes
An address that does not answer with a calendar is refused with 422
calendar_subscription_unreadable, and one you already subscribe to with 409calendar_subscription_exists.One person may subscribe to 20 calendars.
Also available in
calendar.update_subscription()
Change a calendar subscription
def update_subscription( id: str, patch: CalendarSubscriptionPatch, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarSubscriptionResourceRenames a subscription or gives it another colour.
Parameters
idstrRequiredSubscription id,
csb_followed by 24 hex characters.patch['name']strThe new name, up to 100 characters.
patch['color']CalendarColorThe new colour, from
CALENDAR_COLORS.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarSubscriptionResource as it stands now.
Example
from nextmail import nextmail subscription = nextmail.calendar.update_subscription( 'csb_3f9a1c7e5b2d48a06c1e9f4b', {'name': 'Team rota', 'color': 'purple'}) print(subscription['name'], subscription['color'])Notes
A subscription that is not yours is a 404
calendar_subscription_not_found.
Also available in
calendar.refresh_subscription()
Refresh a calendar subscription
def refresh_subscription( id: str, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarSubscriptionResourceReads a subscribed calendar again now instead of waiting for the next half-hourly read, and restarts one that stopped after repeated failures.
Parameters
idstrRequiredSubscription id,
csb_followed by 24 hex characters.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarSubscriptionResource after the read, with status and lastError.
Example
from nextmail import nextmail subscription = nextmail.calendar.refresh_subscription('csb_3f9a1c7e5b2d48a06c1e9f4b') if subscription['lastError']: print('Could not read it:', subscription['lastError'])Notes
A calendar read less than a minute ago is refused with 429
calendar_subscription_refresh_too_soon.
Also available in
calendar.delete_subscription()
Unsubscribe from a calendar
def delete_subscription( id: str, *, api_key: str | None = None, timeout: float | None = None,) -> DeletedCalendarSubscriptionResourceRemoves a subscription and every event it brought onto your calendar.
Parameters
idstrRequiredSubscription id,
csb_followed by 24 hex characters.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
DeletedCalendarSubscriptionResource with deleted set to True.
Example
from nextmail import nextmail deleted = nextmail.calendar.delete_subscription('csb_3f9a1c7e5b2d48a06c1e9f4b') print(deleted['id'], deleted['deleted'])Notes
A subscription that is not yours is a 404
calendar_subscription_not_found.
Also available in
calendar.import_ics()
Import a calendar file
def import_ics( data: RawBody, *, filename: str | None = None, api_key: str | None = None, timeout: float | None = None,) -> CalendarImportResourceSends an iCalendar (.ics) file as the request body and queues it. Its events become events of your own that you can edit, and nobody on them is sent anything, now or when you change them later, unless you ask for that on the change. Poll get_import until status is completed.
The file may be 10 MB and hold 25,000 events. Several calendars in one file are merged, and the report keeps their names. An event whose UID is already in the workspace is skipped and counted, never overwritten, so sending the same file twice adds nothing.
data is bytes, a bytearray or a memoryview, and it goes out as text/calendar.
Parameters
dataRawBodyRequiredThe .ics file as
bytes, abytearrayor amemoryview.filenamestrThe name to show for the file in the list of imports, at most 255 characters.
api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarImportResource with status set to queued.
Example
from pathlib import Path from nextmail import nextmail queued = nextmail.calendar.import_ics(Path('work.ics').read_bytes(), filename='work.ics') print(queued['id'], queued['status'])Notes
A body that is not an iCalendar file is a 400
not_a_calendar_file, an empty one a 400item_import_empty, and one holding more than 25,000 events a 400too_many_events.A file over 10 MB is a 413
item_import_too_large.To send text you already hold, such as what
get_event_icsreturns, encode it first withtext.encode().An upload that outlasts the client's
timeoutraisesNextmailNetworkErrorwithis_timeout, so pass a longertimeout=for a large file on a slow connection.Not retried automatically by the SDK.
Also available in
calendar.list_imports()
List calendar imports
def list_imports( *, limit: int | None = None, cursor: str | None = None, api_key: str | None = None, timeout: float | None = None,) -> Page[CalendarImportResource]Returns one page of the calendar files and calendars brought in, newest first, each with what it added and what it left out. Beside the files sent with import_ics it lists the calendars that came with a mailbox import, and those carry the id of that import in parentImportId.
Paging is keyset. limit= takes 1 to 100 and defaults to 25, and nextCursor goes back as cursor= while hasMore is True. list_all_imports and iterate_imports do that walk for you.
Parameters
limitintImports per page, a whole number from 1 to 100. The server defaults to 25.
cursorstrThe
nextCursorfrom the previous page. Never build one yourself.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
Page[CalendarImportResource], a dict with items, hasMore and nextCursor.
Example
from nextmail import nextmail page = nextmail.calendar.list_imports(limit=50) for item in page['items']: counts = item['counts'] print(item['fileName'], item['status'], counts['imported'], counts['skipped'])Notes
sourceisfilefor an upload.takeout,caldavandmicrosoftcame with a mailbox import.A cursor that names no import is a 400
invalid_cursor.A GET is retried automatically on network failure and on 408, 500, 502, 503 and 504 responses, up to the client's
max_retries, and on a 429 only when it carries aRetry-Afterof a minute or less.
Also available in
calendar.list_all_imports()
Collect every calendar import into one list
def list_all_imports( *, limit: int | None = None, cursor: str | None = None, api_key: str | None = None, timeout: float | None = None,) -> builtins.list[CalendarImportResource]Follows nextCursor from page to page and returns every calendar import in one list, newest first.
Everything is held in memory before the call returns. Prefer iterate_imports when you can stop early. limit= sets the page size of each request, not the total.
Parameters
limitintPage size per request, from 1 to 100, defaulting to 25 on the server.
cursorstrA cursor from an earlier page to start after.
api_keystrOverrides the client's API key for every page of this walk.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
list[CalendarImportResource] holding every import across all pages.
Example
from nextmail import nextmail imports = nextmail.calendar.list_all_imports()added = sum(item['counts']['imported'] for item in imports) print(f'{added} events came from {len(imports)} imports')Notes
A failure on any page raises out of the whole call, and the imports already fetched are discarded.
Also available in
calendar.iterate_imports()
Stream calendar imports one at a time
def iterate_imports( *, limit: int | None = None, cursor: str | None = None, api_key: str | None = None, timeout: float | None = None,) -> Iterator[CalendarImportResource]Returns a generator that yields calendar imports one at a time, newest first, and requests the next page only once the current one is used up. Nothing is fetched until the loop starts, and leaving the loop with break stops the requests.
Parameters
limitintPage size per request, from 1 to 100, defaulting to 25 on the server.
cursorstrA cursor from an earlier page to start after.
api_keystrOverrides the client's API key for every page of this walk.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
Iterator[CalendarImportResource], a generator yielding one import per step.
Example
from nextmail import nextmail for item in nextmail.calendar.iterate_imports(): if item['status'] != 'running': continue print('still running', item['id'], item['counts']['total']) breakNotes
The generator is lazy, so an abandoned loop costs only the pages you consumed.
Also available in
calendar.get_import()
Get a calendar import
def get_import( id: str, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarImportResourceReturns one calendar import with its counts and its report: the calendars it read, how many events it left out and why, and up to 20 of the events it left out. Poll it after import_ics until status is completed or failed.
counts['imported'] is what was added and counts['skipped'] what was left out, which report['skipped'] breaks down by reason: uid-taken for an event that was already here, no-start for one with no start time, invalid for one that could not be read and empty for a file or calendar that held no events.
Parameters
idstrRequiredCalendar import id,
iimp_followed by 24 hex characters.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarImportResource with status, counts, report, undoable and when it started, finished and was undone.
Example
from nextmail import nextmail item = nextmail.calendar.get_import('iimp_3f9c2a7b1e4d8f60a5c7b92d')counts = item['counts']already_here = item['report']['skipped'].get('uid-taken', 0) print(item['status'], counts['imported'], counts['skipped'], already_here) for sample in item['report']['samples']: print(sample['name'], sample['reason'])Notes
An id that names no calendar import is a 404
item_import_not_found, which raisesNextmailApiErrorwithis_not_found.A GET is retried automatically on network failure and on 408, 500, 502, 503 and 504 responses, up to the client's
max_retries, and on a 429 only when it carries aRetry-Afterof a minute or less.
Also available in
calendar.undo_import()
Remove what a calendar import added
def undo_import( id: str, *, api_key: str | None = None, timeout: float | None = None,) -> CalendarImportResourceDeletes every event the import added, changed since or not, and marks the import undone. Nobody is told. Events that were skipped because they were already here are not touched.
Parameters
idstrRequiredCalendar import id,
iimp_followed by 24 hex characters.api_keystrOverrides the client's API key for this call only.
timeoutfloatSeconds this call may take, the response included, before it raises
NextmailNetworkErrorwithis_timeout. It overrides the client'stimeoutfor this call, and0turns the limit off.
Returns
CalendarImportResource with status set to undone and undoneAt set.
Example
from nextmail import nextmail undone = nextmail.calendar.undo_import('iimp_3f9c2a7b1e4d8f60a5c7b92d') print(undone['status'], undone['undoneAt'], undone['counts']['imported'])Notes
An import that is still running, or was already removed, is a 409
item_import_not_undoable.undoableon the import says whether the call would remove anything.An id that names no calendar import is a 404
item_import_not_found.Not retried automatically by the SDK.