Case Study · API Workarounds

Google Calendar API: How I Added Custom Fields When the API Did Not Support Them

A 2015 Google Calendar API workaround for custom fields, kept as a case study in how I extend third-party APIs beyond what they officially support.

Published
Author
A de Villiers
Read
approximately 6 min
Contents
  1. What the API actually returned in 2015
  2. The workaround: encode in description, decode on read
  3. Technical detail (the read path)
  4. The image attachment trick (Flickr API)
  5. The date-range pattern (one year + one month)
  6. Why this matters in 2026
  7. How I think about third-party API workarounds now

In 2015 I integrated the Google Calendar API for an events website and ran into the limitation every developer hits when building on someone else's API: it does not return the fields the business actually needs.

The Google Calendar API v3 returned a fixed set of fields per event. Summary, description, start, end, location, a few attributes. That was it. The business needed cost per event, contact person, contact email, and an attached image. None of those were API-supported fields.

This is the same shape of problem I solve today, just at different scale and with different stacks. The 2015 fix is preserved here as a case study in how to think about a third-party API that does not give you what you need.

What the API actually returned in 2015

The Google Calendar API v3 returned this shape per calendar entry:

{
  "kind": "calendar#calendarListEntry",
  "etag": etag,
  "id": string,
  "summary": string,
  "description": string,
  "location": string,
  "timeZone": string,
  "summaryOverride": string,
  "colorId": string,
  "backgroundColor": string,
  "foregroundColor": string,
  "hidden": boolean,
  "selected": boolean,
  "accessRole": string,
  "defaultReminders": [...],
  "notificationSettings": {...},
  "primary": boolean,
  "deleted": boolean
}

Useful for a calendar UI. Not useful for an events business that needs to display cost, contact details, and a feature image per event.

Google Calendar Labs widgets (which let you attach things in the UI) did not surface those values through the API. The Labs page itself was clear about it: experimental features, may break, may disappear.

The workaround: encode in description, decode on read

The description field accepted free text. The trick was to define a delimiter and a known field order, store the structured data inside the description in the calendar UI, and parse it back out when reading from the API.

Field order chosen for this integration:

Image | Cost | Description | Contact Number | Email Address

Sample description value (what the calendar admin actually typed in the Google Calendar UI):

Flickr_photo_id_302494 | R4000 | Stay at the luxurious Cape Hotel
in Cape Town for an unforgettable weekend and experience the white
sandy beach in Clifton under the African sun. | 021 000 1000 |
info@capem.com

When the API returned the event, the description field came back as a single string. The Node-equivalent of explode("|", $event['description']) split it into a real five-field array on the application side.

Technical detail (the read path)

The PHP code that consumed the API and split the description into structured fields, included here for reference:

public function eventId($eventid) { $service = $this->googleCalendar(); $calendarId = (CALENDAR_ID != "") ? CALENDAR_ID : 'primary'; $event = $service->events->get($calendarId, $eventid); $this->eventsInfo($event); } public function eventsInfo($event) { $desc = explode("|", $event['description']); $desc['photo_id'] = trim($desc[0]); $desc['cost'] = trim($desc[1]); $desc['text'] = $desc[2]; $desc['contact'] = trim($desc[3]); $desc['email'] = trim($desc[4]); for ($i = 0; $i < count($desc); $i++) { unset($desc[$i]); } $image = $this->getflickrImage($desc['photo_id']); $html['event_id'] = $event['id']; $html['start_time'] = $event['start']['date']; $html['end_time'] = $event['end']['date']; $html['image'] = $image; $html['summary'] = $event['summary']; $html['location'] = $event['location']; $html['date'] = $event['start']['date']; $html['cost'] = $desc['cost'] = ($desc['cost'] == 'no') ? '' : $desc['cost']; $html['text'] = $desc['text'] = ($desc['text'] == 'no') ? '' : $desc['text']; $html['contact'] = $desc['contact'] = ($desc['contact'] == 'no') ? '' : $desc['contact']; $html['email'] = $desc['email'] = ($desc['email'] == 'no') ? '' : $desc['email']; $this->htmlData[] = $html; }

The 'no' sentinel handled events where a field was deliberately empty. The Flickr lookup ran against a separate API to fetch the actual image from a photo ID stored in the description.

The image attachment trick (Flickr API)

Google Calendar in 2015 did not support image attachments via the API. The workaround: a dedicated Flickr account holding the event images, photo IDs stored in the calendar description field, and a Flickr API call at read time to construct the image URL.

public function getflickrImage($photoID) { $f = new phpFlickr(FLICKR_KEY); $photoInfo = $f->photos_getInfo($photoID); $photoFarm = $photoInfo['photo']['farm']; $photoServerId = $photoInfo['photo']['server']; $photoId = $photoInfo['photo']['id']; $photoSecret = $photoInfo['photo']['secret']; $photoSize = 'm'; return 'https://farm'.$photoFarm.'.staticflickr.com/' .$photoServerId.'/'.$photoId.'_'.$photoSecret.'_'.$photoSize.'.webp'; }

Two APIs, one stitched-together result. Not elegant. It worked.

The date-range pattern (one year + one month)

For displaying upcoming events grouped by month for the next 13 months:

public function getYearRange() { $service = $this->googleCalendar(); $calendarId = (CALENDAR_ID != "") ? CALENDAR_ID : 'primary'; $startDate = date('Y-m-d'); $y = (int) date('Y') + 1; $m = (int) date('m') + 1; $m = ($m == 13) ? 1 : $m; if (strlen((string)$m) == 1) $m = '0'.$m; $endDay = date("t", strtotime($y.'-'.$m.'-01')); $endDate = $y.'-'.$m.'-'.$endDay; $optParams = [ 'maxResults' => '10000', 'orderBy' => 'updated', 'timeMin' => $startDate.'T00:00:00.000Z', 'timeMax' => $endDate.'T23:59:59.999Z', ]; $events = $service->events->listEvents($calendarId, $optParams); // ... iterate and group by month }

Standard timeMin / timeMax filtering. The interesting part was working out the correct end-date for "one year plus one month" without a date library - date-handling in 2015 PHP was its own adventure.

Why this matters in 2026

I would not build this integration the same way today. The shape of the answer is the same - extend a third-party API beyond what it officially supports - but the modern execution is cleaner:

  • A thin Node.js service between the calendar and the consuming app.
  • A real schema in Postgres for the structured fields, not a delimited string.
  • The Google Calendar API used as a sync source, not the source of truth.
  • A typed interface to the consuming app instead of a raw decoded array.

The reason to keep this 2015 post live is not the code - it is the pattern of thinking. When a third-party API does not give you what the business needs, you have two choices: bend the API (this post), or stand up a parallel system that holds the structure cleanly (what I do now).

The same pattern shows up in the custom Node.js + Postgres reporting layer I built over MemberPress and in the WordPress-to-business-systems API integration work I document elsewhere.

How I think about third-party API workarounds now

Three questions, in this order:

  1. Will the third-party API still exist in two years, in this shape? Google Calendar has changed three times since this post. Anything that depends on the description-field-as-database trick breaks the day Google enforces stricter content rules on the field.
  2. Is the structured data important enough to live somewhere stable? If the answer is yes, the description-field trick is a stopgap. The real answer is a Node.js layer plus a Postgres table you control.
  3. What happens when the third-party API ships the feature you wrote a workaround for? If you encoded into the description field, you have to migrate. If you built a parallel system, you flip a sync direction.

For the 2015 events website, the answer was correct for the time and budget. For most 2026 businesses I work with, the correct answer is the parallel-system approach. Both are still valid; the right call depends on the business.

If you have a third-party API that does not expose what you need, the engagement starts with that decision, not with code. Get in touch and we will work out which version of the answer applies. Past project work is on /projects.

Frequently asked questions

Is this 2015 workaround still the way to do it in 2026?

Treat this post as a case study, not current best practice. The Google Calendar API has changed multiple times since 2015. Today I would solve the same problem with a thin Node.js layer between the calendar source and the consuming app, hold the structured data in my own Postgres, and use the Calendar API only as a sync surface. The pattern in this post is still useful when you need a quick result on a third-party API that does not expose what you need.

What kind of third-party API integrations do you ship now?

Custom Node.js API servers, microservices, and integration layers between business systems. Examples: enterprise rewards APIs, Shopify-to-ERP synchronisation, WordPress/WooCommerce-to-CRM bridges, MemberPress-to-Postgres reporting layers. The shape of the work is always the same: take an API that does not do what the business needs, and build the layer that makes it.

Can you build custom integrations for APIs that do not have the fields I need?

Yes. Most third-party APIs are built for the average customer and stop short of what specific businesses actually need. The work is to either (a) extend on top, like the workaround in this post, or (b) build a parallel system that holds the extra structure cleanly. Which one is right depends on whether the third-party API can be relied on long-term. Decided in the consultation.

Have a project in mind?

I review every enquiry personally. Tell me what you want to build and I'll tell you on the call if it's a fit.

Get in touch