Skip to main content

· 3 min read
Josh Twist

The day comes for most startups, even those that aren’t API-first SaaS businesses. When a large partner or customer — who can’t use your UI at the scale they need — requests an API. This is a high-quality problem - a customer that integrates with your API has higher switching costs and is more likely to be retained.

Sharing an API is a non-trivial exercise that can eat a surprising amount of your eng team’s time and there are three pillars that you need as a minimum bar:

authentication, documentation, protection

1/ Authentication

How will the partner authenticate securely with your API?

Most startups go with API-key authentication because it’s secure and the easiest to use for developers (more on this here) - this is the right choice in my experience. There’s a lot to consider when building a secure API-key solution:

  • Where do I store the keys and do so securely?
  • How do I let partners self-serve?
  • Can partners easily roll keys to ensure best practice security?
  • How do I implement read-once key infrastructure for best-practice security?

This can take even the best engineering teams multiple weeks to ship, and be an ongoing burden to maintain and scale that will reduce your team's agility.

2/ Documentation

How will the developer learn how to use your API?

Your partner’s developers will need documentation to learn how to use your API. Maybe a shared google doc is enough? But, your engineering team will spend much less time helping partner eng folks if they have real API docs - generated using open standards like Open API - that have integrated test clients and API keys. This will save your team time and your partner’s eng team - they’ll thank you for it!

3/ Protection

How do you stop a rogue for-loop in your partners’ code from taking down your whole business?

A partner hitting your API with a Denial of Service attack is rarely a deliberate, malicious act. Rather, it’s probably a simple coding error that results in an infinite loop that - without protection - can take down your API, or your whole business. That’s why rate-limiting is an essential part of any shared API.

Wait... there are more than three pillars?

Those three pillars are just the basics - Ideally, you have a strategy around versioning, analytics, composition, routing, caching, and have the right abstraction to deal with unexpected needs from new partners.

Any API program eventually runs into a customer that needs JWT or mTLS security - will your solution easily allow the layering of different security options? Can you easily maintain both versions of your API? Can you quickly implement a brown-out to push partners onto v2?

· One min read
Josh Twist

We recently shared some reasoning on why we think API keys are the best authentication approach for your public API.

We think this is so important that we built it as a feature of the Zuplo gateway. Our API Key management includes:

  • secure storage and management of keys and metadata - with an admin UI and API to manage consumers.
  • integrated developer portal with self-serve key management for your customers.

| Note, if you've already built your own API Key solution, and have a database store with your keys and users, we can easily integrate zuplo authentication with custom policies. It's never too late to make hosting your API much easier.

See it all in action in this 2-minute video:

Try it out now, for free at

· 2 min read
Josh Twist

Have you noticed something the best API companies have in common?

Stripe, SendGrid, Twilio and Airtable logos

Folks like Stripe, Twilio, Airtable, SendGrid, and many more?

Yep, they all use API Keys as the primary authentication method for their APIs. Why is this?

There are two primary reasons

1/ Security

While there is no formal protocol for API Keys and most implementations have some level of variability - at least compared to standards like OAuth and OpenID Connect - they offer a good level of security, arguably greater than using JWT tokens for a few reasons.

  • Revocability - API Keys can be quickly revoked for any reason, whereas JWT tokens are hard to revoke and often require the reset of an entire certificate or tenant.
  • Opaqueness - unlike JWT tokens, which can be easily decoded using services like, API keys are completely opaque and don’t reveal any hint of your internal authorization mechanism.
  • Self-management - a good API program with API keys allows consumers to manage API keys themselves and, in the event of a leak (or accidental push to a GitHub repo), the consumer can quickly revoke and roll their keys. If the same mishap occurs with a JWT token, it is typically harder for the consumer to self-serve and revoke the validity of the JWT token.

2/ Optimizing Time to First Call (TTFC)

Great API companies focus on developer experience and obsess about metrics like time-to-first-call. That is, how long does it take for a developer to find your documentation and get everything set up, and successfully invoke your API.

If you choose an authentication option that has some complexity (token acquisition, token refresh, etc) you are automatically increasing that Time to First Call and thus reducing the conversion rate of developers that get to know and adopt your platform.


Of course, there are valid reasons to use a more complex authentication method for your API, particularly if the consumer is doing work on behalf of another identity (e.g. accessing the GitHub API on behalf of a GitHub user) then OAuth makes the most sense.

However, if you’re primarily identifying the B2B business partners, API keys are a secure choice that is easy to use and will optimize the conversion funnel for winning new developers.

· 2 min read
Josh Twist

AKA Why you need rate-limiting, and ideally Dynamic Rate Limiting.

Length: 2 minutes

Before launching any API program you need to think about protection. Many API developers don't think they need to worry about rate-limiting because they aren't a target for hackers. That's probably true in the early days, but it isn't the hackers that are going to DoS you; it's your prized customers.

The most common type of API abuse isn't malicious, it's accidental. It's a misplaced for-loop in your partners code that takes you down. This happens often.

So ideally your API has some per-user rate limits. This is super easy with Zuplo.

However, in reality this isn't enough. You should probably have different rate-limits for different customers. For example, your free customers should have a more restrictive policy while your paid, or premium customers a more lenient one.

Guess what? That's also easy with Zuplo because you can write custom code that tells the rate-limit policy how to work. In the video above we show how you can modify the policy limits based on customer type (in this example, stored in the API Key metadata, but could be based on JWT claim, or even a cache-able DB lookup if so required).

Here's the code from request-limit-lookup.ts file in the video:

import { CustomRateLimitDetails, ZuploRequest } from "@zuplo/runtime";

const requestsPerMinute = {
premium: 3,
free: 1,

export default function (request: ZuploRequest): CustomRateLimitDetails {
const customerType =;
const reqsPerMinute = requestsPerMinute[customerType];

const rateLimitConfig = {
// The key tells the rate limiter how to correlate different requests
key: request.user.sub,
requestsAllowed: reqsPerMinute,
timeWindowMinutes: 1,

return rateLimitConfig;

And the config for the rate-limit policy

"export": "RateLimitInboundPolicy",
"module": "$import(@zuplo/runtime)",
"options": {
"rateLimitBy": "function",
"identifier": {
"module": "$import(./modules/request-limit-lookup)",
"export": "default"

Stay safe out there folks!

· 2 min read
Nate Totten

Unfortunately, the Cloudflare Pages integration for GitHub doesn't support GitHub Deployment status, this means you can't trigger actions on deployment_status. deployment_status is the ideal event to use when running tests after a deployment as the event details include the URL. This is how we run tests on Vercel deployments after they are finished.

Fortunately, there is a pretty hacky way to do this with Cloudflare Pages. The check_run event is fired by the pages integration. The check run provides an output object in the event. Unfortunately, the contents is meant for human readable output. It looks like this:

"output": {
"annotations_count": 0,
"annotations_url": "",
"summary": "<table><tr><td><strong>Latest commit:</strong> </td><td>\n<code>be76cc6</code>\n</td></tr>\n<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>\n<tr><td><strong>Preview URL:</strong></td><td>\n<a href=''></a>\n</td></tr>\n</table>\n\n[View logs](\n",
"text": null,
"title": "Deployed successfully"

With a little bit of scripting though it is possible to extract the URL from that summary string. I used grep and cut to do that with the following:

echo "${{ github.event.check_run.output.summary }}" | grep -o "href\=.*>https" | cut -c 7-43

Note the 7-43 at the end is the start index and end index of the string to pull from the grep result. Those numbers need to be adjusted depending on the length of your deployment URL name.

To use the value, just extract it into an environment variable and you can use it later steps in your action.

types: [completed]

name: Test
runs-on: ubuntu-latest

- uses: actions/checkout@v3
- name: Get Deployment URL
run: echo "DEPLOYMENT_URL=$(echo "${{ github.event.check_run.output.summary }}" | grep -o "href\=.*>https" | cut -c 7-43)" >> $GITHUB_ENV
- run: echo $DEPLOYMENT_URL

Not a super elegant solution, but it works. At least until Cloudflare changes that status message.

· 6 min read
Nate Totten

Authorizing other services (i.e. "machines") - sometimes called M2M - to call your API is typically done with either JWT tokens or API Keys. The reason to use one or the other varies by use case. This post will explain the pros and cons of each and suggest when each one is a good fit for securing your API.

JWT Based Machine to Machine (M2M) Authentication

JWT authentication typically uses an OAuth 2.0 identity provider such as Auth0, AWS Cognito, etc. The identity provider issues tokens after validating the clients are who they say they are.

Client Credentials sequence

When the client sends a request to the API it includes the JWT in the request's Authorization header. The API then validates the JWT to be authentic and uses the information in the JWT to identify the client. Typically the JWT contains a sub parameter that identifies the client. The token also includes a aud parameter that specifies which API the token can call.

JWT tokens can be issued with any length of expiration time, but it is typical for tokens to expire in a short period, such as one hour.

JWT auth with OAuth uses the Client Credentials flow on the identity server. Each client that will call the API is issued a Client Id and a Client Secret - think of these values like a username and password. The client uses these values to request an access token they use to call the API. In code, the client credentials flow looks like the following example.


curl --request POST \
--url 'https://YOUR_DOMAIN/oauth/token' \
--header 'content-type: application/x-www-form-urlencoded' \
--data grant_type=client_credentials \
--data client_id=YOUR_CLIENT_ID \
--data client_secret=YOUR_CLIENT_SECRET \
--data audience=YOUR_API_IDENTIFIER


"access_token": "eyJz93a...k4laUWw",
"token_type": "Bearer",
"expires_in": 86400

Considerations of Machine-to-Machine JWT Auth

JWT-based API auth is a good choice for securing microservices within an organization, or sharing APIs with certain types of external clients.

  • JWT tokens are typically not revokable. To revoke a JWT token you typically have to roll the secrets of that client - this will disable ALL JWT tokens currently issued.
  • Permissions with JWT tokens are managed at the identity provider level, meaning that all tokens issued for the same client will have the same permissions.
  • JWT tokens are static; permissions, expiration time, or other properties cannot change once the token is issued.
  • When JWT tokens expire, the consumer must request a new token using the Client ID and Secret value.
  • Identity Providers often charge based on the number of tokens issued.
  • The contents of a JWT token are visible to anyone, they can be decoded using public tools like

API Key Authentication

With API Key authentication, each client receives a unique secret key. Unlike JWT tokens, the key itself doesn't contain any actual data, it is simply an opaque unique string associated with the client. Furthermore, there is no standard protocol for API Key authentication like OAuth, etc., so each implementation can differ.

Ideally, an API using key-based authentication offers the API consumer the ability to manage their keys. For example, an API Gateway could offer a self-serve portal where end-users issue their own tokens and critically can revoke old, and create replacement keys on demand. Tokens can be issued with various permissions and with custom expirations times.

A typical API Key authentication system will validate each key as it comes in with a request. If the key is valid, then data is returned with that key - typically information about their identity and permissions.

// pseudo-code to check key and get metadata
function myApiHandler(request) {
const apiKey = request.headers.get("API-Key");
const apiKeyInfo = apiKeyService.validate(apiKey);

if (!apiKeyInfo.isValid) {
return new Response("Unauthorized", {
status: 401,

// Check various properties of the api key info
if (apiKeyInfo.accountId) {
// ...

Or, when using Zuplo's API Key system:

export default async function (request: ZuploRequest) {
// policy has already enforced that user must
// be authenticated
if ( {
// ...

Considerations of API Key Auth

The main difference between API Key auth and JWT token auth is that the JWT Token is self-contained - the information asserted by the token is in the token. Whereas with an API Key the asserted information is stored in an external system. The externalization of assertion data makes API Keys more flexible for certain scenarios.

  • API Keys tend to be easier to work with for your partners, that's one of the reasons why businesses like Stripe, Twilio and Airtable use API Keys for their public API.
  • Individual API Keys can be revoked - rather than resetting a whole client/customer.
  • Permissions and expiration times of keys can be changed even after they are issued.
  • API keys are opaque, so no details of your implementation or scoping system are visible externally.
  • Because the key doesn't contain any information, the associated data for each key can effectively be limitless. For example, an API Key Authentication system could also assert that a particular token is allowed to access a particular account.
  • API Keys can be issued without expirations and revoked only when needed (i.e., a customer cancels their account).


Both JWT authentication and API Key authentication are good options when building a secure API. Each has benefits and drawbacks. JWT authentication is standardized and there are libraries you can use to implement API key authentication quickly. However it is typically more complex for your API consumers.

API Key authentication, on the other hand, tends to be extremely simple for developers to understand and implement and is popular with B2B SaaS businesses.

However, it can be non-trivial to implement an API Key management solution. You need to securely store (or hash) the API Keys, have a developer-facing UI where consumers can self-serve and roll keys on demand. We've written about our [Best Practices for API Key Authentication] ( developed from building Zuplo and our team's collective experience at companies like Microsoft, Facebook, Auth0, and Stripe.

About Zuplo

Zuplo is a serverless API Gateway, designed for developers. With Zuplo you can secure your API with API Keys, add rate limiting, get developer documentation, and more in record time. Try Zuplo Free

· One min read
Josh Twist

· One min read
Josh Twist

Length: 1 minute

Maybe my favorite video of the series. So if you use AWS Lambda you're probably using the AWS API Gateway. We're sorry to hear that. But fear not, there is an alternative. A real gateway with more features than you can shake at a fresh copy of Monkey Island that outperforms AWS API Gateway calling Lambda? It can't be... can it?

· One min read
Josh Twist

Length: 2 minutes

Zuplo is so fast and flexible, it is the easiest way to setup a mock API. Here we create a simple todo API (of course 🤦🏼‍♂️). We add our 'sleep' policy to make it slow too - so you can call this from your UI client and simulate long loading times.

Here's the code for the request handler:

import { ZuploContext, ZuploRequest } from "@zuplo/runtime";

export default async function (request: ZuploRequest, context: ZuploContext) {
return [
{ text: "Learn Javascript", done: false },
{ text: "Learn Typescript", done: false },
{ text: "Play around in Zuplo", done: true },
{ text: "Build something awesome", done: true },

Have fun, APIFiddling!