Docs

Exclusif sits between your sign-in and your app. Your sign-in provider says who someone is; Exclusif says whether they're in and what they get. Your backend asks once per login, and Exclusif answers from rules and budgets you set in the dashboard.

Quickstart

1. Create an account and a project

Sign up with your email address. During early access, new accounts are approved by hand, so you may get an email when yours is ready rather than right away. Then create a project for your app. A project has its own access rules, budget, and keys.

2. Copy your runtime key

Setting up your first project shows its runtime key once. It starts with exr_. Store it as a server-side secret, for example EXCLUSIF_RUNTIME_KEY. It must never reach a browser. Create more keys, and revoke old ones, from the project's Keys tab.

3. Install the SDK

npm install @exclusif/sdk

The SDK is a thin wrapper over the HTTP API. Any language that can make an HTTPS request can integrate without it.

4. Ask Exclusif at sign-in

After your own sign-in succeeds, call claim with the identity your provider gave you. autoRequest: true puts someone who isn't in yet into line under your project's access rules.

import { createExclusifClient } from '@exclusif/sdk/node';

// Public keys for verifying signed passes. Cache them; they rarely change.
const keys = await fetch('https://exclusif.vip/api/public/pass-keys')
  .then((res) => res.json());

export const exclusif = createExclusifClient({
  baseUrl: 'https://exclusif.vip',
  runtimeKey: process.env.EXCLUSIF_RUNTIME_KEY,
  projectSlug: 'my-app',
  passPublicKeys: keys.byKid,
});

// In your sign-in handler:
const decision = await exclusif.claim({
  identityProvider: 'firebase',
  identitySubject: user.uid,
  identityEmail: user.email,
  autoRequest: true,
});

5. Act on the decision

Every answer is a normal 200 response with a status. Errors such as a bad key or a network failure throw instead, so never treat an exception as a decision.

switch (decision.status) {
  case 'admitted':
    // decision.terms is locked in. Store it with the user.
    saveEntitlement(user, decision.terms, decision.termsHash);
    break;
  case 'request_pending':
  case 'request_waitlisted':
    showWaitingScreen();
    break;
  default:
    showNoAccess(); // no_admission, request_rejected, revoked, expired
}

For a credit budget, the grant is in decision.terms.grant.amountMicros, in millionths of a US dollar ($5.00 is 5000000). Your app keeps track of what each user spends against it; Exclusif is not called on every use.

6. Decide what happens if Exclusif is unreachable

Store the decision when a user is first admitted, and let people who are already in keep signing in from your own records. Only new sign-ups then depend on Exclusif being available. A pure access fence can fail closed; a promotional grant should usually fail open.

7. Let people ask for access

Turn on your hosted join page and link to https://exclusif.vip/join/your-project from your site. When you approve someone, the approval email links to your app's sign-in, and the claim above lets them in.

Access rules

Each project has one access rule. You can change it at any time.

RuleWhat happens to a new person
Open until fullGets in while seats remain, then joins a first-in, first-out waitlist.
WaitlistJoins the waitlist. You let people in by adding seats, oldest first.
Hand-pickedWaits for you to approve or reject them in the dashboard.
Invite codeGets in only with a valid code passed to claim as inviteCode.

When you add seats to a project with a waitlist, the dashboard shows exactly who will get in before you confirm, and nobody can jump the line in between. Rejected and revoked people stay out: autoRequest never reopens them.

Credit budgets

A project hands out one of three kinds of terms:

KindWhat a person gets
Access onlyEntry, with no credits attached.
AI creditsA dollar grant from a budget you set.
Usage quotaCounts you define, such as 20 searches, for the first N people.

An AI-credit budget can hand out grants in two ways:

  • Shrinking grants. The first person gets the most and each person after gets a fixed amount less. When the budget is spent, new people still get in with access only.
  • Fixed tiers. For example, the first 100 people get $5 and the next 400 get $2. When the budget is spent, new claims are refused until you raise it.

Grants are locked in when they're given. Raising a budget never changes what earlier people received, and an amendment applies only to people who arrive after it. You can also restrict which models a grant may be spent on and split it across providers.

Sign-in decisions

StatusMeaning
admittedIn. Includes terms, termsHash, passToken, claimIndex, and expiresAt.
request_pendingWaiting for your review.
request_waitlistedIn line for a seat.
no_admissionNot in, and no request exists. Invite-code projects answer this until a valid code is used.
request_rejectedYou rejected their request. Final.
revokedYou took their access back. Final.
expiredTheir grant expired. With autoRequest they go through your access rules again as a newcomer.

Use checkAdmission (POST /api/v2/admissions/check) to read someone's status without creating a request or a pass.

Signed passes

An admitted decision includes a passToken: an ES256-signed token that expires after 15 minutes. It's useful when the service that grants credits is not the one that called claim. Verify it offline with the public keys from GET /api/public/pass-keys:

const pass = exclusif.verifyPass(decision.passToken);
// pass.sub is the admission id, pass.terms the locked-in terms.

The pass is a one-time hand-off, not a session. Keep your own session for the signed-in user.

Hosted join page

Every project can have a page at https://exclusif.vip/join/your-project where people ask for access. Turn it on in the project's Gate tab and set its headline and description. The page shows seats left when your project has a seat limit, and checks for bots before accepting a request.

What happens next follows your access rule: people get in right away while seats remain, join the waitlist, or wait for your review. Exclusif emails them at each step, and the approval email links to the sign-in address you set for the project.

Prefer your own form? Send the same request from your backend with POST /api/public/projects/your-project/requests.

Email and your domain

Exclusif emails people when they ask for access, join a waitlist, get approved, or are rejected. You can edit every email and send yourself a test from the dashboard's Email tab.

By default these emails come from hello@exclusif.vip under your project's name, with your reply-to address. To send from your own domain, connect a Resend account in the Email tab:

  1. In Resend, verify your domain and create an API key.
  2. Add a Resend webhook for all email events, pointing at the address the Email tab shows, and copy its signing secret.
  3. Enter your sender address, the key, and the secret.

Exclusif sends one check message through your account and switches your project over only if Resend accepts it, so a domain that isn't verified yet can't break your email. The key and secret are stored encrypted and never shown again, and bounces and complaints are tracked per project. Disconnect at any time to go back to the default sender.

Webhooks

Register an HTTPS endpoint in the Webhooks tab to hear about changes as they happen:

  • request.created and request.decided
  • admission.claimed, admission.revoked, and admission.expired
  • admission.reenrollment_authorized and identity.reset

Deliveries are signed in the Standard Webhooks format and retried with backoff. Verify the raw body before parsing it, and ignore event ids you've already handled:

import { verifyAndParseWebhookEvent } from '@exclusif/sdk/node';

const event = verifyAndParseWebhookEvent({
  secret: process.env.EXCLUSIF_WEBHOOK_SECRET,
  headers: req.headers,
  rawBody, // the exact bytes received
});

HTTP API

Base URL https://exclusif.vip. Runtime calls send the runtime key in the x-exclusif-key header, and projectSlug in the body must match the key's project.

CallPurpose
POST /api/v2/admissions/claimSign-in decision. Body: projectSlug, identityProvider, identitySubject, identityEmail, optional autoRequest and inviteCode.
POST /api/v2/admissions/checkSame decision, read-only.
GET /api/public/pass-keysPublic keys for verifying passes. No key needed.
GET /api/public/projects/:slug/statusAccess rule and seats left, for your own landing page. No key needed.
POST /api/public/projects/:slug/requestsAsk for access. Body: email, optional fields, and turnstileToken. Always answers 202.
curl https://exclusif.vip/api/v2/admissions/claim \
  -H "x-exclusif-key: $EXCLUSIF_RUNTIME_KEY" \
  -H "content-type: application/json" \
  -d '{"projectSlug":"my-app","identityProvider":"firebase",
       "identitySubject":"uid-123","identityEmail":"ana@example.com",
       "autoRequest":true}'

Questions or problems: privacy@exclusif.vip.