Seth Carney
October 06, 2020

Recently, we’ve been working on standardizing some aspects of Courier’s REST APIs, such as naming conventions and HTTP status response codes. While Courier has an extensive web UI where our Design Studio lives, we have multiple APIs behind the scenes that do a lot of the heavy lifting. It’s important to make sure these APIs are as developer-friendly as possible, meaning they should be semantically consistent and predictable. I thought I’d share some of our guiding principles and learnings that might be useful for your team as well.
As with most things in life, when it comes to writing a REST API, consistency is key. A REST API is a communication layer, and like any language, it has certain rules and conventions that should be followed in order for things to run smoothly. The different methods (GET, POST, DELETE, etc.) have specific behaviors that users will expect the API to follow. HTTP response codes are standardized as well so that users know what the problem was and can take some action depending on the response.
Deviations from the norm or inconsistencies within the API itself means there most likely will be differences between what the user expects to happen, and what the API’s actual behavior looks like. As a developer, you’ve probably experienced this (we definitely have), where the way that you call one part of the API looks completely different from the way you make a nearly identical call in another part of the API. In the worst case, this means your users end up with buggy code, and in the best case, it means they are going to have to branch their code to handle any special cases, making it harder for them to maintain the integration. Plus, any nonstandard aspects of your API will also make it harder for your own team to manage and update it.
When an API call is made, the server returns a three-digit HTTP status code in its response. These codes start in the 100s and go to the 500s. A lot of the work we’ve done recently has been to standardize these responses so that they fully describe Courier’s behavior in every case.
Codes in the 200s indicate that the client’s request was successful. Within this category, there are several possible statuses. The ones we care about at Courier are 200, 202 and 204.
A 200 response code means that you were successful, *and *you can expect more information in the response body (such as the data that you requested with a GET). To create a more developer-friendly API, we’ve been moving some of these generic 200 responses to more specific 202 or 204 responses. A 202 means that Courier has accepted the request, but hasn’t been able to process it yet. This is mostly used for our Send pipelines and tracking. And a 204 means that the request was successful, but there’s no data to return back in the body. This might happen for example if you DELETE a resource; beyond acknowledging that this was successful, there’s nothing else that the user expects to receive in that moment from Courier.
A note about status code 201: officially this code means that a resource was created, but we’ve found that in practice most APIs just return a 200 for this, since the fact that you’re using the API with an action like POST means that your intention is to create a resource.
As we get into error conditions, response codes become even more useful. 4xx codes describe various types of client errors that can happen (for Courier, usually this is a data validation error), while 5xx codes are for server-side errors (something happened with Courier’s backend that caused the request to fail).
One thing we’ve found useful is to develop a consistent and standard error interface, so that when you’re integrating with Courier, you can always expect that same structure for an error response. This looks like:
Copied!
{code?: string; // a string indicating the error codedoc_url?: string; // an optional link to documentationmessage?: string; // a human-readable error messagetype: // the type of error returned| “api_error”| “authentication_error”| “authorization_error”| …}
By defining an opinionated error response type it allows Courier to convey further information to the developer which could be used to fix and resubmit the request.
If you want to make your API easier to expand in the future, it’s important to accept and return objects that encapsulate the raw data. Even if you’re just returning something like a message ID, returning an object will help you make sure that additions to the response later on don’t break anything for your legacy users.
For example, instead of returning a message tracking ID like this:
“4bccd5bf-3bdc-49f4-b2d7-7851338bd105”
With objects at the root level, your API would look more like this:
Copied!
{messageId “4bccd5bf-3bdc-49f4-b2d7-7851338bd105”}
When you API is idempotent, this means that retries won’t affect the results in unexpected ways. For example, if you request to send an email through an API, and don’t receive a response for some reason (a network error, for example), you might not know what to do. Should you resend the request and risk sending duplicate emails? If the API is idempotent, you don’t have to worry, because a retry will not result in duplication.
At Courier, we support idempotency on send requests through an Idempotency-Key header, which signals to Courier that multiples of the same request should be treated as idempotent
I hope this article has been helpful.

Your AI agent already knows how to use a terminal. Why CLIs beat MCP servers
LLMs already know how to use a terminal. We maintain both an MCP server and a CLI at Courier, and the difference tells you something about where AI tooling is heading.
By Mike Miller
March 13, 2026

EU Data Residency for Notifications: What Engineering Teams Need to Know
Courier supports EU data residency through a dedicated datacenter in AWS EU-West-1 (Ireland), with full API feature parity, same-workspace dual-region access, built-in GDPR deletion endpoints, and localization support for multilingual notifications. Engineering teams can switch to EU hosting by changing a single base URL with no workspace migration or downtime required.
By Kyle Seyler
March 09, 2026

Expo Push Notifications: The Complete Implementation Guide (SDK 52+)
Expo push notifications are alerts sent from a server to a user's phone, even when the app isn't open. To set them up, install the expo-notifications library, ask the user for permission, and get a unique push token for their device. Your server sends a message to Expo's push service with that token, and Expo delivers it through Apple or Google. Push notifications only work on real phones, not simulators. Local notifications are different — they're scheduled by the app itself for things like reminders. You can also route Expo push through services like Courier to add email, SMS, and Slack fallbacks.
By Kyle Seyler
February 24, 2026
© 2026 Courier. All rights reserved.