Base URL:
- production:
https://www.shift2bikes.org/api/ - local development:
https://localhost:4443/api/
Most responses are in JSON format, except for:
- event export returns vCalendar format
- event crawl returns HTML
Endpoint:
- GET
events
Example requests:
/events.php?id=1234/events.php?startdate=2019-06-01&enddate=2019-06-15
URL parameters:
id:caldailyevent ID- if
idis provided it takes precedence; all other params will be ignored
startdate:- first day of range, inclusive
YYYY-MM-DDformat- if not provided, current date is used
enddate:- last day of range, inclusive
YYYY-MM-DDformat- if not provided, current date is used
all:- for range requests only (no effect when
idis provided) - if
true, delisted events are included, with minimal information provided in their event object; useful for clients that cache results and need to reconcile events that have been removed
- for range requests only (no effect when
Unknown parameters are ignored.
It is recommended that you always provide either an event id or both startdate and enddate. Relying on default or inferred values may return unexpected results.
Success:
- status code:
200 events: array of event objects; array may be empty- each event object: key-value pairs of all available public fields; does not contain any private fields (use
manage_eventendpoint for those) - when using
idparameter, array is expected to return 1 object; if the ID does not match a known event, you will receive a200response with an emptyeventsarray
Example response for a single event:
{
"events": [
{
"id": "6245",
"title": "Shift to Pedalpalooza Ride",
"venue": "director park",
"address": "877 SW park",
"organizer": "fool",
"details": "Have you ever wondered how Pedalpalooza happens every year...and did you know we have a team of programmers who work on the shift calendar and website. There is a lot of rewarding volunteer work that goes on behind the scenes and we are recruiting for new folks who are interested in helping out next year and beyond. Come on this ride and we will talk a little bit about the history of shift and try to find you a place to help out in the future. We will end at a family friendly watering hole. First round of drinks is on shift. We will be done by 8 so you can check out other rides.",
"time": "18:00:00",
"hideemail": "1",
"length": null,
"timedetails": null,
"locdetails": null,
"eventduration": "120",
"weburl": null,
"webname": "shift",
"image": "/eventimages/6245-3.jpg",
"audience": "G",
"tinytitle": "shift2pedalpalooza",
"printdescr": "learn how to get involved with shift and pedalpalooza",
"datestype": "O",
"area": "P",
"featured": false,
"printemail": false,
"printphone": false,
"printweburl": false,
"printcontact": false,
"email": null,
"phone": null,
"contact": null,
"date": "2017-06-05",
"caldaily_id": "9300",
"shareable": "https://shift2bikes.org/calendar/event-9300",
"exportable": "https://shift2bikes.org/api/ics.php?event_id=9300",
"cancelled": false,
"newsflash": null,
"status": "A",
"endtime": "20:00:00"
}
]
}
Example response for a range of events:
{
"events": [
{
"id": "1234",
...
},
{
"id": "1236",
...
},
{
"id": "2200",
...
}
],
"pagination": {
"start": "2024-07-01",
"end": "2024-07-10",
"range": 10,
"events": 3,
"prev": "https://www.shift2bikes.org/api/events.php?startdate=2024-06-21&enddate=2024-06-30",
"next": "https://www.shift2bikes.org/api/events.php?startdate=2024-07-11&enddate=2024-07-20"
}
}
Example response for a range of events, including delisted events:
{
"events": [
{
// for scheduled and canceled events,
// all event fields are provided
"id": "9000",
"title": "Early Bird New Year's Day Ride",
... // all standard fields
"date": "2025-01-01",
"caldaily_id": "10101",
"shareable": "https://shift2bikes.org/calendar/event-10101",
"cancelled": false,
"newsflash": null,
"status": "A",
"endtime": "10:00:00"
},
{
// for delisted occurrences when there are still valid occurences in the series,
// all event fields are provided
"id": "9001",
"title": "Late-Riser New Year's Day Ride",
... // all standard fields
"date": "2025-01-01",
"caldaily_id": "10102",
"shareable": "https://shift2bikes.org/calendar/event-10102",
"cancelled": true,
"newsflash": "New start time",
"status": "D", // D=delisted
"endtime": null
},
{
// for delisted occurences when the entire event series has been deleted,
// only these limited fields are provided
"id": "9002",
"deleted": true,
"date": "2025-01-01",
"caldaily_id": "10103",
"shareable": "https://shift2bikes.org/calendar/event-10103",
"cancelled": true,
"newsflash": null, // always null when delisted
"status": "D", // D=delisted
"endtime": null
}
// if an event is deleted when it is unpublished (aka hidden),
// the event is entirely gone and is not included here
],
"pagination": {
"start": "2025-01-01",
"end": "2025-01-01",
"range": 1,
"events": 3,
"prev": "https://www.shift2bikes.org/api/events.php?startdate=2024-12-31&enddate=2023-12-31",
"next": "https://www.shift2bikes.org/api/events.php?startdate=2025-01-02&enddate=2025-01-02"
}
}
Errors:
- status code:
400 error: object containingmessagekeymessage: text string explaining the error- possible errors
enddatebeforestartdate- date range too large (100 days maximum)
Example error:
{
"error": {
"message": "enddate: 2019-06-01 is before startdate: 2019-06-15"
}
}
Images must be fetched separately from the initial event request; each event object provides a URL to its image, if it has one.
Some best practices for handling images:
- Only fetch images if you plan to use them. Especially for range requests, you may not need images for every event in the range.
- Clients should be able to handle image request failures (e.g. 404, 429, or 5xx errors).
- Caching is encouraged. The image file has change number after the event ID, e.g.
/eventimages/6245-3.jpg. This number is incremented only when the image has changed. If the change number is the same, you can safely use a cached image.
Endpoint:
- GET
ics
Example request:
/ics.php?event_id=9300/ics.php?series_id=6245/ics.php?startdate=2019-06-01&enddate=2019-06-15
URL parameters:
event_id:caldailyID (single occurrence)- if
event_idis provided it takes precedence; all other params will be ignored
series_id:caleventID (all events in a series)
id:- alias for
series_id - deprecated; clients should use
event_idorseries_idfor clarity
- alias for
startdate:- first day of range, inclusive
YYYY-MM-DDformat- if not provided, current date is used
enddate:- last day of range, inclusive
YYYY-MM-DDformat- if not provided, current date is used
all:- for range requests only (no effect when
idis provided) - if
true, delisted events are included; useful for clients that cache results and need to reconcile events that have been removed
- for range requests only (no effect when
filename:- if
none, the output is plain text instead of an ICS file; useful for local debugging
- if
If multiple ID/range parameters are provided, they take precedence with decreasing specificity: event_id, series_id, startdate/enddate.
If no parameters are provided, it responds with a range from 1 month prior to 6 months forward from the current date.
Unknown parameters are ignored.
Example response for a single event:
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//shift2bikes.org//NONSGML shiftcal v2.1//EN
METHOD:PUBLISH
X-WR-CALNAME:Shift Community Calendar
X-WR-CALDESC:Find fun bike events all year round.
X-WR-RELCALID:community@shift2bikes.org
BEGIN:VEVENT
UID:event-9300@shift2bikes.org
SUMMARY:Shift to Pedalpalooza Ride
CONTACT:fool
DESCRIPTION:Have you ever wondered how Pedalpalooza happens every
year...and did you know we have a team of programmers who work on the
shift calendar and website. There is a lot of rewarding volunteer work
that goes on behind the scenes and we are recruiting for new folks who are
interested in helping out next year and beyond. Come on this ride and we
will talk a little bit about the history of shift and try to find you a
place to help out in the future. We will end at a family friendly
watering hole. First round of drinks is on shift. We will be done by 8
so you can check out other rides.\nhttps://shift2bikes.org/calendar/event-
9300
LOCATION:director park\n877 SW park
STATUS:CONFIRMED
DTSTART:20170606T010000Z
DTEND:20170606T030000Z
CREATED:20230512T085747Z
DTSTAMP:20170512T050134Z
SEQUENCE:1
URL:https://shift2bikes.org/calendar/event-9300
END:VEVENT
END:VCALENDAR
Errors:
- status code:
400,404 - possible errors
event_idorseries_idnot foundenddatebeforestartdate- date range too large (100 days maximum)
Endpoint:
- GET
crawl
Example request:
/crawl.php?id=1234
URL parameters:
id:caldailyevent ID
Unknown parameters are ignored.
This endpoint is used by web crawlers such as search engines.
Success:
- status code:
200 - returns a simple HTML rendering of ride data
- if
idparameter is not present, a short, general message about Shift
Example response:
<html>
<head>
<title>Shift to Pedalpalooza Ride</title>
<meta property="og:title" content="Shift to Pedalpalooza Ride">
<meta property="og:url" content="https://www.shift2bikes.org/calendar/event-9300">
<meta property="og:image" content="https://www.shift2bikes.org/eventimages/6245.jpg">
<meta property="og:type" content="article">
<meta property="og:description" content="Have you ever wondered how Pedalpalooza happens every year...and did you know we have a team of programmers who work on the shift calendar and website. There is a lot of rewarding volunteer work that goes on behind the scenes and we are recruiting for new folks who are interested in helping out next year and beyond. Come on this ride and we will talk a little bit about the history of shift and try to find you a place to help out in the future. We will end at a family friendly watering hole. First round of drinks is on shift. We will be done by 8 so you can check out other rides.">
<meta property="og:site_name" content="SHIFT to Bikes">
<meta name="description" content="Have you ever wondered how Pedalpalooza happens every year...and did you know we have a team of programmers who work on the shift calendar and website. There is a lot of rewarding volunteer work that goes on behind the scenes and we are recruiting for new folks who are interested in helping out next year and beyond. Come on this ride and we will talk a little bit about the history of shift and try to find you a place to help out in the future. We will end at a family friendly watering hole. First round of drinks is on shift. We will be done by 8 so you can check out other rides.">
<meta name="keywords" content="bikes,fun,friends,Portland,exercise,community,social,events,outdoors">
</head>
<body>
<h2>Mon, Jun 5th, 6:00 PM - Shift to Pedalpalooza Ride</h2>
<p>Have you ever wondered how Pedalpalooza happens every year...and did you know we have a team of programmers who work on the shift calendar and website. There is a lot of rewarding volunteer work that goes on behind the scenes and we are recruiting for new folks who are interested in helping out next year and beyond. Come on this ride and we will talk a little bit about the history of shift and try to find you a place to help out in the future. We will end at a family friendly watering hole. First round of drinks is on shift. We will be done by 8 so you can check out other rides.</p>
<p>877 SW park</p>
<img src="https://www.shift2bikes.org/eventimages/6245.jpg">
</body>
</html>
Errors:
- status code:
404 - body of response is empty
- possible errors
idnot foundidof a hidden (unpublished) event
Endpoint:
- GET
retrieve_event
URL parameters:
id:caleventevent IDsecret: event password
The retrieve_event endpoint returns all private data for the event (if the secret is provided) so it can be edited. If you just want to retrieve public data to display the event, use the event endpoint.
Success:
- status code:
200 - key-value pairs of all available fields; the response is similar to the
eventendpoint's event object, but note that they are not identical (thedatestatusesblock, for example) - if a valid
secretis provided, all stored values are returned; if not, you still get a200response but private fields (e.g.email) will be empty
Example response:
{
"id": "6245",
"title": "Shift to Pedalpalooza Ride",
"venue": "director park",
"address": "877 SW park",
"organizer": "fool",
"details": "Have you ever wondered how Pedalpalooza happens every year...and did you know we have a team of programmers who work on the shift calendar and website. There is a lot of rewarding volunteer work that goes on behind the scenes and we are recruiting for new folks who are interested in helping out next year and beyond. Come on this ride and we will talk a little bit about the history of shift and try to find you a place to help out in the future. We will end at a family friendly watering hole. First round of drinks is on shift. We will be done by 8 so you can check out other rides.",
"time": "18:00:00",
"hideemail": "1",
"length": null,
"timedetails": null,
"locdetails": null,
"eventduration": "120",
"weburl": null,
"webname": "shift",
"image": "/eventimages/6245-3.jpg",
"audience": "G",
"tinytitle": "shift2pedalpalooza",
"printdescr": "learn how to get involved with shift and pedalpalooza",
"datestype": "O",
"area": "P",
"featured": false,
"printemail": false,
"printphone": false,
"printweburl": false,
"printcontact": false,
"email": "user@example.com",
"phone": null,
"contact": null,
"datestatuses": [
{
"id": "9300",
"date": "2017-06-05",
"status": "A",
"newsflash": null
}
]
}
Errors:
- status code:
400 - possible errors
- no
idspecified idnot found
- no
Endpoint:
- POST
manage_event
Request can be sent as JSON, or as multipart/form-data containing binary image data plus JSON.
URL parameters:
- none
Request body:
- required fields
id:caleventevent ID (blank when creating an event, required when updating); set by the server on create; ignored if provided by the user when creating a new eventsecret: event password (required only whenidis provided); set by the server, ignored if provided by the user when creating a new eventtitle: event namedetails: event descriptionorganizer: organizer nameemail: organizer emailvenue: location nameaddress: location address; should be mappable with online map services (e.g. Google Maps) or be a valid http/s URLtime: event start timedatestatuses: array of datestatus objects, one for each event occurrenceid:caldailyoccurrence ID (blank when creating an occurrence, required when updating); set by the server, ignored if provided by the user when creating a new occurrencedate: event date, YYYY-MM-DD formatstatus:A(active, aka scheduled; default) orC(cancelled)newsflash: brief message unique to the occurrence; optional
code_of_conduct: boolean; organizer must agree to Shift's Code of Conductread_comic: boolean; organizer must confirm they have read the Ride Leading Comic
- optional fields
audience:G(General; default),F(Family-friendly),A(Adults-only)safetyplan: boolean; if the organizer pledges to follow Shift's COVID Safety Planarea:P(Portland; default),V(Vancouver WA),W(Westside),E(East Portland),C(Clackamas)timedetails: any additional time details, e.g. if there is a separate meet time and ride timeeventduration: duration, in minuteslocdetails: any additional time details, e.g. meet by the tennis courtslocend: end location details; can be any description, does not have to be mappable (e.g. "Outer Southeast" or "near Beaverton Transit Center")loopride: boolean; if ride end location is the same as the startlength: length of ride, in miles;--for unspecified, or0-3,3-8,8-15, or15+weburl: http/s URL, e.g.https://pdx.social/@shift2bikeswebname: friendly URL name, e.g.@shift2bikes@pdx.socialphone: organizer phone numbercontact: any additional contact info for the organizer; can be a name, an email address, another web URL, social media link, PO Box, or anything elseimage: URL to the event image, e.g./eventimages/12345.jpg; set by the server, ignored if provided by the user (see note below for how to add an image)tinytitle: short event name (max 24 characters); used for the Pedalpalooza print calendar, and in some places online where space is tight (e.g. month view); if this is not provided, the first 24 characters of thetitlefield will be automatically copied into this fieldprintdescr: short event description (max 120 characters); used for the Pedalpalooza print calendar, and in some places online where space is tighthideemail: boolean; don't list organizer's email online; default truehidephone: boolean; don't list organizer's phone number onlinehidecontact: boolean; don't list organizer's additional contact info onlineprintemail: boolean; include organizer's email in the print calendarprintphone: boolean; include organizer's email in the print calendarprintweburl: boolean; include organizer's email in the print calendarprintcontact: boolean; include organizer's email in the print calendarfeatured: boolean; featured ride status, set by admins and ignored if provided by the userpublished: boolean; set by the server, ignored if provided by the user
Unknown properties are ignored.
Example JSON request:
{
"id": "99999",
"secret": "1234567890abcdef1234567890abcdef",
"title": "Fun Bike Ride",
"details": "Funtime biketime get your bike fun on",
"audience": "G",
"time": "5:00 PM",
"timedetails": "",
"eventduration": "",
"area": "P",
"venue": "TBA",
"address": "TBA",
"locdetails": "",
"locend": "",
"length": "--",
"organizer": "Josh",
"email": "test@test.test",
"hideemail": "1",
"webname": "",
"weburl": "",
"phone": "",
"contact": "",
"tinytitle": "Fun Bike Ride",
"printdescr": "So much fun!",
"code_of_conduct": "1",
"read_comic": "1",
"datestatuses": [
{
"id": "",
"date": "2024-07-07",
"status": "A",
"newsflash": ""
}
]
}
Success:
- status code:
200 - if a valid
idis provided (to update an event), a validsecretmust also be included - response body is the same as the
retrieve_eventendpoint
Errors:
- status code:
400 - possible errors
- no request body or not parseable JSON
- required field was not included, or has an invalid value
- invalid
secret(when updating)
Example error:
{
"error": {
"message": "Invalid secret, use link from email"
}
}
To add an image to the event, use a multipart/form-data request and send the image as binary. Accepts gif, jpeg, pjpeg, and png images, up to 5 MB.
Example multipart/form-data request:
-----------------------------1234123412341234123412341234
Content-Disposition: form-data; name="file"; filename="image.jpg"
Content-Type: image/jpeg
<binary image data>
-----------------------------1234123412341234123412341234
Content-Disposition: form-data; name="json"
{ "id":"99999", ..., "datestatuses": [ { ... } ]}
-----------------------------1234123412341234123412341234--
Success:
- status code:
200 - response body is the same as when submitting a JSON-only request
Errors:
- status code:
400or413 - possible errors
- unsupported file type (
400) - file size too large (
413)
- unsupported file type (
Example error:
{
"error": {
"message": "There were errors in your fields",
"fields": {
"file": "The file uploaded is not an image"
}
}
}
Endpoint:
- POST
delete_event
URL parameters:
- none
Request body:
- required fields
id:caleventevent IDsecret: event password
- optional fields
- none
Unknown properties are ignored.
Example request:
{
"id": "6245",
"secret": "example"
}
Success:
- status code:
200 successtrue message
Example response:
{
"success": true
}
Errors:
- status code:
400 - possible errors
- no request body or not parseable JSON
idnot included- invalid
id - invalid or missing
secret
Example error:
{
"error": {
"message": "Invalid secret, use link from email"
}
}
- 1.0.0: From Shift formation (c. 2002) until v2 (mid-2017)
There were undoubtedly revisions during this time, but changelog documentation is not available.
- 2.0.0: (2017-06-09) Launch of the
/funmobile-friendly calendar view, added to the existing PHP-based site
As with v1, there were probably revisions to v2 during this time, but changelog documentation is not available.
- 3.0.0: (2019-03-14) Launch of Hugo-based site; the API is now fully separated from the front-end
- 3.0.1: (2019-03-25) Fixed ICS export
- 3.0.2: (2019-04-01) Events and ICS endpoint documentation
- 3.1.0: (2019-04-11) Maximum day range (45 days) for Events endpoint
- 3.2.0: (2019-04-18) Must agree to Code of Conduct to create an event
- 3.3.0: (2020-02-02) Added more details to ICS export
- 3.3.1: (2020-03-05) Bug fix for user edits overwriting the "featured" event status set by admins
- 3.3.2: (2020-04-23) Added temporary blanket cancellation to Events endpoint
- 3.3.3: (2020-05-21) Bug fixes and documentation for the Retrieve Event
- 3.4.0: (2020-05-25) Hidden events are excluded from the Events and Crawl endpoints; added error checks on Crawl endpoint
- 3.4.1: (2020-05-30) Error handling and documentation for Delete Event endpoint
- 3.5.0: (2020-06-05) Publishing an event requires second post to Manage Event endpoint
- 3.5.1: (2020-06-07) Updated blanket cancellation date on Events endpoint
- 3.5.2: (2020-08-13) Removed blanket cancellation from Events endpoint
- 3.6.0: (2021-03-28) Print details are no longer required to submit an event
- 3.7.0: (2021-04-21) Increased max day range on Events endpoint to 100 days, to accommodate 3-month Pedalpalooza
- 3.8.0: (2021-05-19) Added "loop ride" and "location end details" fields
- 3.9.0: (2021-05-28) Added "COVID safety plan" field
- 3.10.0: (2023-05-11) iCal feed improvements: better handling of cancelled or deleted events; adds more info to each event
- 3.10.1: (2023-05-16) Bug fix for soft deleting event occurrences
- 3.10.2: (2023-05-18) Better enforcement of max image size
- 3.11.0: (2023-10-09) Cache busting for updated event images
- 3.12.0: (2024-01-22) New values for Area field (Westside, East Portland, Clackamas); added pagination object to Events endpoint response when requesting a range of events
- 3.12.1: (2024-02-12) Manage Event (create/update) endpoint documentation
- 3.13.0: (2024-03-04) Event info now accepts UTF-8 characters (previously limited to latin1 ASCII)
- 3.50.0: (2024-04-22) Backend now running on Node instead of PHP; no (planned) breaking changes
- 3.50.1: (2024-04-29) Improved validation of data payloads for manage/delete event endpoint requests
- 3.50.2: (2024-05-06) Fixed handling of some boolean fields which may unexpectedly be null (hidden, highlight, printemail, etc)
- 3.51.0: (2024-05-20) Removed now-unused PHP
- 3.52.0: (2024-06-04) Added
all=trueparam to the events and ICS endpoints to include delisted events with a range request. For the events endpoint, minimal information will be provided in the event object if an event is delisted; useful for clients that cache results and need to reconcile events that have been removed. Also added afilename=noneparam to the ICS endpoint as a debugging tool, and improved line-wrapping in the iCal feed event descriptions. - 3.53.0: (2024-06-24) Increased event image size limit to 5 MB (previously 2 MB).
- 3.54.0: (2024-07-02) Migrated to latest MySQL LTS version (v8.4).
- 3.55.0: (2024-08-30) Added year-round calendar iCal feed (at
/api/shift-calendar.php), in addition to Pedalpalooza-specific one - 3.55.1: (2024-12-09) Terms fields (
code_of_conduct,ride_comic) are now only validated on initial submission - 3.56.0: (2024-12-13) Max day range is now set in config;
prevURL added topaginationobject; paginationrangenow reports an inclusive number of days (e.g. single day range now returnsrange: 1instead of0) - 3.56.1: (2025-03-03) Updated dependencies: nginx (patch)
- 3.56.2: (2025-03-24) Updated dependencies: Node.js (patch) plus 1 of its dependencies
- 3.56.3: (2025-04-07) Updated dependencies: MySQL (patch)
- 3.57.0: (2025-06-23) Altered weburl field to allow 512 characters (up from 255)
- 3.58.0: (2025-08-11) Added experimental
ride_countendpoint: provides the number of events in a given time frame, excluding cancelled events. Syntax & usage may not be stable yet. - 3.58.1: (2025-09-15) Updated dependencies: nginx
- 3.59.0: (2025-09-23) Fixed issue with ride length field; now saves, retrieves, and displays correctly
- 3.59.1: (2025-09-29) Updated MySQL patch version
- 3.59.2: (2025-10-20) Fixed some backend tests
- 3.59.3: (2025-11-03) Adjusted search results order when searching past events
- 3.59.4: (2025-11-17) When mapping an event location, "TBA"/"TBD" addresses are now handled more robustly. Search endpoint now looks at either A) today and future, sort order ascending (default), or B) past only, sort order descending.
- 3.59.5: (2025-11-24) Updated dependencies: http-proxy-middleware
- 3.59.6: (2025-12-01) Updated dependencies: Vite
- 3.59.7: (2025-12-08) Updated dependencies: express, validator, multer
- 3.59.8: (2025-12-12) Remove now-unneeded version from Docker compose file
- 3.59.9: (2025-12-22) Updated dependencies: nodemailer
- 3.59.10: (2026-01-15) Changed dependency management to only allow patch updates; updated dependencies: MySQL. Also removed unused example data.
- 3.60.0: (2026-02-19) ICS export now supports either single occurrence (
event_id; new default) or the series (series_id; previous default). The existingidparameter aliases toseries_idfor backwards compatibility, but clients are encouraged to specify the ID type explicitly. Also updated Node to v24.x (latest LTS).