How Stripe designs for dates and times in the API
Tags: webdev, api, stripe
Published: 2022-09-21 14:53:23

## Introduction

APIs are built for developers to use - and *successful* APIs are designed with developer experience in mind. Part of a good developer experience is consistency, whether that’s in the design of the path names (all being either `snake_case` or `kebab-case`) or every entity having an `id` property with its unique identifier. When adding new features to an API, its important to refer to existing patterns so that we can provide a predictable interface. Today you’ll learn the design patterns Stripe engineers use when adding **date and time** fields to the API.

Imagine you want to add a new feature to a Subscriptions API. Users report that one of their challenges is canceling subscriptions at a later date. They are building complex workarounds with cron jobs and would rather tell Stripe when the Subscription should be canceled. Here’s a basic product spec:


> While creating or updating a Subscription with the API, users can pass a date and time in the future when they want the Subscription to cancel and if the user retrieves a Subscription from the API, they know when the subscription was canceled if at all.

Let’s break that down:


- “While creating or updating a Subscription with the API” → this feature will need to work for both create and update endpoints. 
- “users can pass a date and time in the future” → we need to receive the date and time from the user as an argument to the API.
- “when they want the Subscription to cancel” → the new date and time should be stored somewhere so that we can use it in a new background job that will cancel subscriptions.
- “if the user retrieves a Subscription from the API” → this is a different feature for the retrieve endpoint. 
- “they know when the subscription was canceled” → we need to render the date and time that the subscription was canceled. 
- “if at all.” → It’s possible the property on the Subscription that represents when it was canceled is null (if it is not canceled).

This task involves at least three features to implement, and will allow us to talk about both `datetime` arguments to the API and `datetime` properties of objects returned from the API. Before enumerating possible solutions and tradeoffs, let’s talk about some ways that working with date and time is challenging.


## Why working with date and time is challenging


{% twitter https://twitter.com/PetuniaGray/status/1570482462203215872 %}


If you’ve tried to build a calendar interface, or localize a `datetime` to user specific timezones, then some of these challenges are familiar:

### Timezones are surprisingly complex

Writing code to work well with timezones is confoundingly difficult [[0](https://www.zainrizvi.io/blog/falsehoods-programmers-believe-about-time-zones/)], [[1](https://twitter.com/phredmoyer/status/967125751790157824)]. Timezones also add operational overhead. Maybe you’re based in San Francisco and forget to deploy a billing change before your customers in Sydney start getting charged for next month — woops. Here’s a spooky one: in the State of Indiana, timezones differ by county. 


![Map of counties in Indiana with red and yellow coloring based on the timezone of the county https://en.wikipedia.org/wiki/Time_in_Indiana.](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1q7g6h9rc9eesz1g5ezc.png)



### Months have different numbers of days
Because the last day of the month might fall on 28, 29, 30, or 31, it can be challenging to automate tasks to happen “on the last day of the month”. There is certainly some bad code floating around that switches based on the month to derive the last day:


```ruby
require 'time'

def last_day_of_month?
  case Date.today.month
  when 1, 3, 5, 7, 8, 10, 12
    Date.today.day == 31
  when 4, 6, 9, 11
    Date.today.day == 30
  else
    Date.today >= 28 # 🐛
  end
end
```

If you’re not convinced, check out this StackOverflow Question about how to [calculate last day of month](https://stackoverflow.com/questions/222309/calculate-last-day-of-month) with JavaScript. It has been viewed 430,000+ times and has 25 different answers!

Stripe Billing will derive the last day of the month for you, by the way. If a customer’s monthly subscription is [anchored](https://stripe.com/docs/billing/subscriptions/billing-cycle) to August 31st, they will be charged on September 30 and February 28th.


### Local date formats differ by the order of day, month, and year

The ECMAScript standards body maintains an API purely for localizing dates. Here are some examples from the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString):


```javascript
const date = new Date(Date.UTC(2012, 11, 20, 3, 0, 0));
// US English uses month-day-year order and 12-hour time with AM/PM
console.log(date.toLocaleString('en-US'));
// → "12/19/2012, 7:00:00 PM"

// British English uses day-month-year order and 24-hour time without AM/PM
console.log(date.toLocaleString('en-GB'));
// → "20/12/2012 03:00:00"

// Korean uses year-month-day order and 12-hour time with AM/PM
console.log(date.toLocaleString('ko-KR'));
// → "2012. 12. 20. 오후 12:00:00"
```


![Date format usage map from https://en.wikipedia.org/wiki/Date_format_by_country](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4brh441pywz9a4jxa935.png)



| **Color** | **Order styles** | [**End**](https://en.wikipedia.org/wiki/Endianness) |
| --------- | ---------------- | --------------------------------------------------- |
| Cyan      | DMY              | L                                                   |
| Yellow    | YMD              | B                                                   |
| Magenta   | MDY              | M                                                   |
| Green     | DMY, YMD         | L, B                                                |
| Blue      | DMY, MDY         | L, M                                                |
| Red       | MDY, YMD         | M, B                                                |
| Grey      | MDY, YMD, DMY    | M, B, L                                             |


### Storing local timezones doesn’t scale

If you use a local timezone for a `datetime` stored in your database, then attempting to scale across multiple regions introduces new problems.

### 12 hour vs. 24 hour clocks

The 24 hour clock is the standard, but the 12 hour clock is more dominant in countries that were once part of the British Empire[[2](https://en.wikipedia.org/wiki/12-hour_clock)]. In some places the 12 hour clock is used verbally, but the 24 hour clock is used in written material.

### Leap year

Our trip around the sun is 365 days, 5 hours, 59 minutes and 16 seconds instead of a perfect 365. Leap years in our Gregorian calendar are calculated by adding extra days in each year that is an integer multiple of 4 (except for years evenly divisible by 100, but not by 400) [[3](https://en.wikipedia.org/wiki/Leap_year)]. The Julian calendar from Roman times solved this with a quadrennial intercalary **day** as a 48-hour February 24, see [bissextile](https://en.wiktionary.org/wiki/bissextile)! 

The list in the draft of this article was longer so I hope you’re convinced by this abbreviated version. Next, we’ll discuss API designs that are scalable, avoid timezone trapdoors, leap second pitfalls, and can be consumed by anyone in the world.


## Options for working with dates and times in an API

Most web APIs today use JSON for serialization and deserialization. JSON doesn’t have a native `date`, `time`, or `datetime` scalar data type, so we’ll need to use either String or Number to represent our values. 

### Arbitrary string
As a naïve early-career developer, I would’ve likely designed my internal APIs to work with string values and picked an arbitrary string that was easy to read as a human. Something like: 

`m/dd/yyyy hh:mm:ss`

Pros:

- Easy to read (if you know the format!)

Cons:

- Not a widely accepted format
- No concept of timezone
- Doesn’t localize well to other regions
- Requires knowing whether the middle and last date part are the month or the day. E.g. is `8/9/2022` August 9th or September 8th?
- Slashes in the date mean it does not play nicely with URL paths
- Strings are heavier weight in the database

This is for illustration, **please never do this in your API**. Instead, select a format that is agreed upon by a recognized standards body.

### Separate key and value for each date part

Instead of accepting the `datetime` as a single value, we could accept each part in a structured object. For example:

```json
{
  "id": "sub_abc123", # ID of the Subscription to cancel.
  "cancel_at": {
    "year": 2022,
    "month": 8,
    "day": 9,
    "hour": 8,
    "minute": 54,
    "second": 0,
    "timezone-offset": "+05:00"
  }
}
```

Pros:

- We avoid string format inconsistencies across locales

Cons:

- Users of the API still need to deal with timezones
- While unlikely, the hour passed by a user could mistakenly be 8:54 p.m. a.k.a 20:54
- What if we need sub-second accuracy? You might expect to see `second: 0.00123` etc, or `milliseconds: 123` - neither of which map well to our colloquial understanding of time

Before we completely dismiss this approach, let’s look at an example without the time component. Here’s an example for creating a Person with a name and date of birth:

```json
{
  "first_name": "Jenny",
  "last_name": "Rosen",
  "date_of_birth": {
    "year": 1901,
    "month": 1,
    "day": 9,
  }
}
```

This structure works great for dates without times!

Pros:

- Avoids any confusion about local formatting for dates like `9-1-1901` vs. `1-9-1901`
- Avoids string manipulation
- Simple

Cons:

- Implementation code must handle each part individually instead of one value

### ISO 8601

The International Organization for Standardization (ISO) [[4](https://en.wikipedia.org/wiki/International_Organization_for_Standardization)] defines an “international standard covering the worldwide exchange and communication of date and time-related data” -- ISO 8601 [[5](https://en.wikipedia.org/wiki/ISO_8601)]. The remarkably comprehensive specification and includes support for durations and time intervals.

Here are some examples of a `datetime` with timezone in ISO 8601 format:

```
2022-09-12T14:42:04+00:00
2022-09-12T14:42:04Z
```

Pros:

- Widely accepted standard

Cons:

- Some developers may resort to string manipulation
- Strings are heavier weight in the database
- Users still need to think about timezones
- More than one value can represent the same moment. E.g. `1994-11-05T08:15:30-05:00`  and `1994-11-05T13:15:30Z` are the same! [[6](https://www.w3.org/TR/NOTE-datetime)]

If this is the option you decide to use, consider accepting arguments with any timezone offset, then only responding with dates and times based in UTC time.

### Unix time

> In computing, **Unix time (also known as Epoch time, Posix time, seconds since the Epoch, Unix timestamp or UNIX Epoch time) is a system for describing a point in time**. It is the number of seconds that have elapsed since the Unix epoch, excluding leap seconds. The Unix epoch is 00:00:00 UTC on 1 January 1970. [[7](https://en.wikipedia.org/wiki/Unix_time)]

```json
{
  "id": "sub_abc123", # ID of the Subscription to cancel
  "cancel_at": 1662738023,
}
```

Pros:

- Unix time does not require users to reason about timezones
- Unix time is a single signed number
    - Because integers are easier to query, index, and more space efficient than strings, dates are often stored as 64-bit integers in the database
    - Use negative numbers for moments before January 1st, 1970
- Unix time has a single format
- Unix time is not effected by daylight savings
- Unix time is a widely supported standard
- Easy to convert to a nice localized format client side using a library like [date-fns](https://date-fns.org/), or [day.js](https://day.js.org/)
- You can get a cool shirt with the current time https://datetime.store/

Cons:

- Not human readable. E.g. how long ago did `1662738023` happen? is it in the future?
- Unix timestamps for dates after 03:14:07 UTC on 19 January 2038 require more than 32 bits to represent. This is known as the [Year 2038 problem](https://en.wikipedia.org/wiki/Year_2038_problem)


## Stripe’s API design patterns for representing dates and times

🟢 **GOOD:** Use **unix timestamps** to represent dates with times

This is the case for most fields and parameters and are represented as a Timestamp fields and parameters in the API. Timestamps in the API are represented as an integer denoting a Unix timestamp, which counts the number of seconds since the Unix epoch.

```bash
curl https://api.stripe.com/v1/subscriptions/sub_1LhfXp2Tb35ankTnpxovpHQs \
  -u sk_test_: \
  -d "cancel_at"=1662738023
```

```json
{
  "id": "sub_1LhfXp2Tb35ankTnpxovpHQs",
  "cancel_at": 1662738023
}
```

🟢 **GOOD:** Use hashes with `day`, `month`, and `year` properties to represent **birth dates** 

Integrations collect birthdays as day, month, and year separately. This hash syntax allows them to construct and read the value without parsing it into a string. See [Issuing Cardholder](https://stripe.com/docs/api/issuing/cardholders/object#issuing_cardholder_object-individual-dob) or [Account](https://stripe.com/docs/api/accounts/object#account_object-individual-dob) as an example of this format.


```bash
curl https://api.stripe.com/v1/accounts/acct_1KtbGiBdyCgEshX8/persons \
  -u sk_test_: \
  -d first_name=Jenny \
  -d last_name=Rosen \
  -d "dob[year]"=1901 \
  -d "dob[month]"=2 \
  -d "dob[day]"=12
```

```json
{
  "id": "person_1LhfZ7",
  "first_name": "Jenny",
  "last_name": "Rosen",
  "dob": {
    "year": 1901,
    "month": 2,
    "day": 12
  }
}
```

🔴 **BAD:** Use any other format representing dates or times.


## Stripe’s API design patterns for naming `datetime` fields

**Timestamp fields are named** `**<verb>ed_at**`

The `_at` pattern signals that the integer is always a unix timestamp. The `_at` suffix is not allowed for non-timestamp fields. Some fields like the `created` timestamp on all resources, do not conform to the `_at` format and likely predated this design pattern. 

🟢 **GOOD:** `subscription.canceled_at`, `invoice.finalized_at`

🔴 **BAD:** `subscription.canceled`, `subscription.canceled_on`, `subscription.cancellation_date`

- For future timestamps, prefer `<verb>s_at`; e.g. `file_link.expires_at`. But beware that future timestamps may imply an SLA.
- This is just a general guideline and there are some good reasons to break it. For example, `dispute_evidence_details.due_by`, `coupon.redeem_by`, and `issuing_card_shipping.eta` do not use the `_at` suffix, but are more clear. 

**Expiration times are represented as** `**expires_at: <unix timestamp>**`

🟢 **GOOD:** `file.expires_at` 

🔴 **BAD:** `file.expires_after`, `file.expiration_date`


- If expiration is handled by a cronjob, it’s okay for that cronjob to lag slightly. E.g. if there’s a `status` field that changes from `pending` to `expired`, and the user fetches the object a little while after `expires_at`, it’s okay if they still see `pending`.
- Double-check `expires_at` when an operation is performed. For instance, if there’s a URL that points to a hosted page, we should check `expires_at` when the hosted page is loaded.
- Why not `expires_after`? It could be mistaken for “expires after N days” rather than “expires at, or very soon after, a given `datetime`.”  It also doesn’t encourage tighter runtime checks mentioned above; users appreciate tighter contracts when we can give them.


## Developer Experience Improvements

### Syntactic Sugar Shortcuts

Stripe also adds a tasty developer experience enhancement for working with dates. Some endpoints accept magic strings in lieu of integer timestamps to make them even easier to work with. For instance, one common use-case when passing datetimes to an API is to pass the current date and time. Rather than needing to calculate that, if you know you always want to pass “now” then you can use the magic string `**now**` e.g. for setting the billing cycle anchor which determines what day each month to charge for a subscription [[8](https://stripe.com/docs/billing/subscriptions/billing-cycle#api-now)]. 

You can imagine other ways this might be extended to accept arguments like `subscription_end` or `tomorrow`. If you have ideas for others we’d love to hear about them in the comments below!

### Test Clocks

Test clocks [[9](https://stripe.com/docs/billing/testing/test-clocks)] are a feature of the API that enable you to simulate moving through time. You create a Test Clock object, then create a related Customer with that Test Clock, then you can advance the clock to a future time to test your webhook handling logic will properly deal with the events that fire for that period.

Here’s a compact example:

```javascript
// Create a test clock
const testClock = await stripe.testHelpers.testClocks.create({
  frozen_time: 1635750000,
  name: 'its a date demo',
});

// Create a customer with the test clock
const customer = await stripe.customers.create({
  email: 'jenny.rosen@example.com',
  test_clock: testClock.id,
  payment_method: 'pm_card_visa',
  invoice_settings: {default_payment_method: 'pm_card_visa'},
});

// Create a subscription for the customer
const subscription = await stripe.subscriptions.create({
  customer: customer.id,
  items: [{
    price: 'price_1LcssrCZ6qsJgndJ2W9eARnK'
  }],
});

// Advance time
const testClock2 = await stripe.testHelpers.testClocks.advance(
  testClock.id,
  {frozen_time: 1641020400}
);
```

### Code samples

How to convert a unix timestamp into a native `date`, `time`, or `datetime` object in your lingua franca.

**Ruby**

```ruby
Time.at(1662759630)
```

**Python**

```python
from datetime import datetime
datetime.utcfromtimestamp(1662759630)
```

**PHP**

```php
DateTime::createFromFormat( 'U', 1662759630 )
```

**JavaScript**

```javascript
new Date(1662759630 * 1000)
```

**Go**

```go
time.Unix(1662759630, 0)
```

**Java**

```java
new java.util.Date((long)1662759630 * 1000);
```

**.NET**

```csharp
DateTimeOffset.FromUnixTimeSeconds(1662759630);
```


## Conclusion

Unix timestamps and structured date of birth objects provide many benefits for API consumers. We’re always flexible and open to adjusting to a better way of doing things. Let us know your preferred approach when working with date and time data in your API. 


## About the author

![CJ Avilla](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mef4k8k1jhnula9b1mxc.png)


[CJ Avilla](https://cjav.dev) ([@cjav_dev](https://twitter.com/cjav_dev)) is a Developer Advocate at Stripe, a Ruby on Rails developer, and a [YouTuber](https://www.youtube.com/CJAvilla). He loves learning and teaching new programming languages and web frameworks. When he’s not at his computer, he’s spending time with his family or on a bike ride 🚲. 


## P.S.

Do you think one day, tombstones will have unix timestamps? 

Jenny Rosen, lived `554805154` - `3067403554`, beware the Ides of March. 


![Drawing of tombstone with unix timestamps for birth and death dates](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9kkz7kjan8n6fsnj0rpi.png)