---
title: "Provision API Keys at First Login"
description: "Most auth providers expose a hook into the signup flow. Use it to create a Zuplo API key consumer the moment a user lands on your developer portal."
canonicalUrl: "https://zuplo.com/blog/2026/05/08/provision-api-keys-at-first-login"
pageType: "blog"
date: "2026-05-08"
authors: "martyn"
tags: "Developers, API Gateway"
image: "https://zuplo.com/og?text=Provision%20API%20Keys%20at%20First%20Login"
---
A new user signs into your developer portal for the first time. They open the
API reference, hit the playground, and there's nothing there. No key. One more
click between them and their first successful request.

That click doesn't need to be there. Most mainstream auth providers expose a
hook that fires when a user signs in, and you can use it to create their API key
before they ever land on the portal. By the time the page renders, the key
already exists.

In a Zuplo developer portal, the payoff lands directly in the playground. The
user signs in, the API playground is already populated with their key, ready to
use, and the first call works without anyone hunting for a button.

This post walks through the pattern using Auth0 Actions, the easiest to wire up.
The closing section covers how the same idea maps to Clerk, Supabase, and
Firebase Auth.

<CalloutAudience
  variant="useIf"
  items={[
    `Running a Zuplo developer portal with custom authentication`,
    `Trying to give every signed-in user a working API key on first load`,
    `Looking to make the "Time to Hello, World" even shorter`,
  ]}
/>

<CalloutVideo
  variant="card"
  title="Provision API Keys at First Login"
  description="Watch the video walkthrough of wiring an Auth0 Post Login Action to the Zuplo Developer API so a new user lands on the portal with a working key."
  videoUrl="https://www.youtube.com/watch?v=lEzZqOmTnNY"
  thumbnailUrl="http://i3.ytimg.com/vi/lEzZqOmTnNY/hqdefault.jpg"
/>

## How the flow works

![A user signs into Auth0; the Post Login Action calls the Zuplo Developer API to create a consumer plus key, then the user is redirected to the developer portal which fetches the key back from Zuplo using the user's email.](/blog-images/2026-05-08-provision-api-keys-at-first-login/diagram-1.png)

Auth0 fires a Post Login Action every time a user signs in. The Action runs
server-side, so it can hold a Zuplo API key as a secret and call the Developer
API on the user's behalf.

On first login, the Action creates a Zuplo consumer, asks for an API key to be
generated alongside it, and stores the consumer's ID in the user's
`app_metadata`. On every subsequent login, it sees the metadata and exits.

That's the whole pattern. One Action, one fetch call, one piece of metadata.

<CalloutTip variant="tip">
  In Zuplo, a **consumer** owns one or more API keys, usually one per end user
  or integration. A **bucket** holds those consumers and is scoped per
  environment (working copy, production, or shared). The **Developer API** is
  Zuplo's management plane at `dev.zuplo.com`, the API you call to create
  consumers and keys programmatically.
</CalloutTip>

## Step 1: Get a Zuplo Developer API key

Open your Zuplo account settings and create an API key for the Developer API.
Copy it. You'll paste it into Auth0 in a moment.

While you're there, note your account name and the bucket you want consumers
created in. Both live under `Services > API Key Service` in the portal.

Buckets are scoped per environment, so pick the one matching the environment
your portal points at. If you run separate Auth0 tenants for preview and
production, set up the Action in each and point it at the matching bucket.

## Step 2: Create the Auth0 Action

In the Auth0 dashboard, go to Actions > Triggers and pick the **post-login**
trigger from the Sign Up & Login section (the one that fires after a user is
authenticated but before the token is issued). Click **Create Action** to
scaffold a new Action under that trigger and name it something like
`create-zuplo-consumer`.

Click the key icon in the editor sidebar and add a secret called `API_KEY` with
the Zuplo Developer API key you copied. Then drop in the following code,
swapping in your account and bucket names:

```js
const ZUPLO_ACCOUNT = "your-zuplo-account";
const API_KEY_BUCKET = "your-bucket-name";

exports.onExecutePostLogin = async (event, api) => {
  // Already provisioned, skip
  if (event.user.app_metadata?.api_consumer) {
    return;
  }

  const body = {
    // UUID keeps the name unique in the bucket
    name: `c-${crypto.randomUUID()}`,
    description: `Consumer for ${event.user.name}`,
    // Portal uses this to match the consumer to the signed-in user
    managers: [event.user.email],
    metadata: {
      // Handy for tracing back to Auth0 later
      user_id: event.user.user_id,
    },
  };

  try {
    const response = await fetch(
      // with-api-key=true mints the key in the same call
      `https://dev.zuplo.com/v1/accounts/${ZUPLO_ACCOUNT}/key-buckets/${API_KEY_BUCKET}/consumers?with-api-key=true`,
      {
        method: "POST",
        body: JSON.stringify(body),
        headers: {
          Authorization: `Bearer ${event.secrets.API_KEY}`,
          "content-type": "application/json",
        },
      },
    );

    if (!response.ok) {
      console.error(response.status);
      throw new Error("Error creating API consumer");
    }

    const result = await response.json();

    // Remember the consumer id so we don't create another next time
    api.user.setAppMetadata("api_consumer", result.id);
  } catch (err) {
    // Don't block login if Zuplo is briefly unreachable
    console.error(err);
  }
};
```

A few things worth pointing out.

The `with-api-key=true` query param tells Zuplo to mint an API key in the same
request. Without it, you'd need a second call. One round trip beats two,
especially inside a login flow.

The `managers` field is the ownership link, not a display field. When the
developer portal authenticates a user, it looks up any consumer whose `managers`
array contains that user's email and renders their keys. That's how the freshly
minted key shows up on the portal page without any extra glue.

Pass multiple emails if more than one identity should own the same consumer. If
you allow sign-in connections that don't return an email (some SAML or social
providers), add an early return for `!event.user.email` before the fetch.

The `try / catch` deliberately swallows errors. If Zuplo is briefly unreachable,
you don't want to lock the user out of the portal. The Action logs the failure
and exits clean. Next login, the metadata check is still empty, and the Action
retries.

One edge case to know about: if the Developer API succeeds but `setAppMetadata`
fails before Auth0 saves it, the next login will create a second consumer for
the same user. The `managers` link still works (the portal sees both), and you
can sweep duplicates on the Developer API. Rare enough to ignore until it isn't.

<CalloutTip variant="mistake">
  This pattern relies on Zuplo's keys being **retrievable**: the key is created
  server-side without ever being shown to the user, and they retrieve it from
  the developer portal on first visit. Buckets configured for irretrievable keys
  (hashed at creation, shown once and never again) won't work, since the user
  would never see the key. Zuplo defaults to retrievable, which is what makes
  this whole flow possible.
</CalloutTip>

## Step 3: Deploy and attach the Action to the trigger

Hit Deploy on the Action. Back on the **post-login** trigger page, drag your new
Action into the flow between Start and Complete and save it.

![The Auth0 Post Login trigger flow with the Create API Key Action placed between Start and Complete.](/blog-images/2026-05-08-provision-api-keys-at-first-login/auth0-post-login-flow.png)

The next time a user signs into your portal, they'll have a consumer and a
working API key before the page finishes loading.

<CalloutTip variant="tip">
  If you also use Zuplo's built-in monetization, skip this entire setup. The
  monetization flow handles consumer creation for you, including assigning users
  to plans and billing them through Stripe.
</CalloutTip>

<CalloutDoc
  title="Create an API Key Consumer on Login"
  description="The full reference for the Auth0 Actions integration, including the Developer API endpoints used."
  href="https://zuplo.com/docs/dev-portal/dev-portal-create-consumer-on-auth"
  icon="book"
/>

## Step 4: Connect Auth0 to your developer portal

The Action provisions the consumer, but the developer portal still needs to know
how to authenticate the same user so the `managers` email match resolves. Open
`zudoku.config.tsx` in your portal project and add an `authentication` block:

```ts
export default {
  // ... other configuration
  authentication: {
    type: "auth0",
    domain: "your-domain.us.auth0.com",
    clientId: "<your-auth0-client-id>",
    audience: "https://your-domain.com/api",
  },
  // ... other configuration
};
```

`clientId` and `domain` come from the Auth0 application's Basic Information
page. `audience` is the identifier of the Auth0 API you create alongside the
application.

Three things have to line up before this works:

- The Auth0 application is a Single Page Web Application with `/oauth/callback`
  registered as an allowed callback URL and `/oauth/logout-callback` as an
  allowed logout URL.
- An Auth0 API exists with the same identifier you pass as `audience`. Without
  it, the portal can't validate tokens Auth0 issues.
- Refresh token rotation is enabled on the application with a 0-second overlap
  period, so the portal can keep the session alive across reloads.

<CalloutDoc
  title="Authenticate with Auth0"
  description="Full Zudoku auth config reference, including callback URLs, the Auth0 API requirement, and refresh token rotation."
  href="https://zuplo.com/docs/dev-portal/zudoku/configuration/authentication-auth0"
  icon="book"
/>

## Verify the Auth0 Action provisioned a key

Sign into your developer portal as a test user. Open the API key management
page. You should see a consumer named `c-<uuid>` with one key attached.

![The Zuplo API Key Service page showing a freshly created consumer named c-0e862097-... with the test user's email and one masked API key.](/blog-images/2026-05-08-provision-api-keys-at-first-login/consumer-in-portal.png)

Check Auth0 > User Management > Users, click your test user, and look at the App
Metadata tab. `api_consumer` should be set to the consumer ID Zuplo returned.
That's the receipt that the Action ran.

Run a request from the portal playground. It should authenticate without
anything else from you.

![The Zuplo developer portal API playground with the user's auto-provisioned consumer pre-selected under Authentication, returning a 200 OK with a JSON list of todos on the first request.](/blog-images/2026-05-08-provision-api-keys-at-first-login/portal-playground.png)

## How this maps to Clerk, Supabase, and Firebase Auth

The mechanism varies by provider, and that variance is where most teams get
stuck:

- **Clerk** fires
  [`user.created` webhooks](https://clerk.com/docs/webhooks/sync-data)
  (eventually consistent, retried on failure). POST to a small endpoint that
  calls the Zuplo Developer API and stores the consumer ID in Clerk's
  `publicMetadata` or your own database.
- **Supabase** offers
  [Auth Hooks](https://supabase.com/docs/guides/auth/auth-hooks) (HTTP endpoints
  or Postgres functions) and Postgres triggers on `auth.users`. The trigger
  pattern is the most direct: insert into a profile table, call out to Zuplo
  from an Edge Function on that table.
- **Firebase with Identity Platform** has
  [Blocking Functions](https://cloud.google.com/identity-platform/docs/blocking-functions)
  on the `beforeCreate` trigger (the v2 SDK helper is `beforeUserCreated`). Same
  shape as Auth0 Actions, deployed as a Cloud Function. Note the 7 second
  response budget.
- **PingFederate** and generic OpenID providers don't expose a hook of this
  kind. The fallback is provisioning on the first authenticated request to your
  portal's backend.

Auth0 is the easiest because Actions are synchronous, run server-side with a
secret store, and don't need a second service to host the handler. The others
get you to the same outcome with more wiring.

## Extend the pattern with tags, metadata, and expiry

The same pattern extends in obvious directions. Add tags to the consumer payload
to mark which Auth0 connection the user came from. Add metadata fields the API
Key Authentication policy can read at request time for backend routing or
per-tenant rate limits.

Use `expiresOn` on the key if you want trial users to lose access after a fixed
window. Mirror the setup with a Post User Deletion Action that calls `DELETE` on
the same consumer endpoint, so revoking a user in Auth0 revokes their API key in
the same step.

First login is the cheapest place to provision. The user is right there, the
identity is fresh, and you have a Zuplo bucket waiting.