Zuplo
Developers

Provision API Keys at First Login

Martyn DaviesMartyn Davies
May 8, 2026
8 min read

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.

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.

Use this approach if you're:
  • 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
Provision API Keys at First Login
Video Tutorial

Provision API Keys at First Login

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.

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.

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.

Pro 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.

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:

Javascriptjs
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.

Common 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.

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.

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

Pro 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.

Create an API Key Consumer on Login

The full reference for the Auth0 Actions integration, including the Developer API endpoints used.

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:

TypeScriptts
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.

Authenticate with Auth0

Full Zudoku auth config reference, including callback URLs, the Auth0 API requirement, and refresh token rotation.

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.

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.

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 (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 (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 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.