Skip to content

Every claim plugin id with its parameters, state, and where it runs, plus the request and response contract for custom plugin endpoints.

A claim is a list of plugin instances. Each plugin checks one criterion and reports pass or fail. This page is the reference for the built-in plugin ids and for the HTTP contract that custom plugins implement. The tutorial for writing one is Build a Claim Plugin.

See the API reference for every route's request and response schema.

Look Up Any Plugin

bash
curl https://api.bitbadges.io/api/v0/plugin/must-own-badges -H "x-api-key: $BITBADGES_API_KEY"
ts
import { BitBadgesAdminAPI, BigIntify } from 'bitbadges';

// searchPlugins is exposed on the admin client, which includes the base API methods.
const BitBadgesApi = new BitBadgesAdminAPI({ convertFunction: BigIntify, apiKey: process.env.BITBADGES_API_KEY });
const { plugin } = await BitBadgesApi.getPlugin('must-own-badges');
const latest = plugin.versions[plugin.versions.length - 1];
console.log(latest.userInputsSchema);     // what the claiming user provides
console.log(latest.publicParamsSchema);   // creator-configured, public
console.log(latest.privateParamsSchema);  // creator-configured, private
console.log(latest.verificationCall);     // HTTP endpoint config
console.log(latest.stateFunctionPreset);  // 'Stateless' | 'ClaimToken' | 'ClaimNumbers' | 'CustomResponseHandler'

// Several at once: POST /api/v0/plugins/fetch
const many = await BitBadgesApi.getPlugins({
  pluginIds: ['must-own-badges', 'min-badge', 'url-clicker'],
  returnSensitiveData: false // true = include pluginSecret (owner only)
});

// Directory search: GET /api/v0/plugins/search
const found = await BitBadgesApi.searchPlugins({ searchValue: 'badge', bookmark: '', locale: 'en' });

// Execution errors for your own plugin: GET /api/v0/plugins/errors
const errors = await BitBadgesApi.getPluginErrors({ pluginId: 'ticket-check', bookmark: '' });

The must-own-badges plugin document (synthesized from the indexer's seed data; timestamps vary):

json
{
  "plugin": {
    "_docId": "must-own-badges",
    "pluginId": "must-own-badges",
    "metadata": {
      "name": "Token Requirements",
      "description": "Which tokens must the user own?",
      "image": "https://bitbadges.io/images/bitbadgeslogo.png",
      "createdBy": "BitBadges",
      "documentation": "https://docs.bitbadges.io"
    },
    "locale": "en",
    "reviewCompleted": true,
    "lastUpdated": "1788652800000",
    "createdAt": "1788652800000",
    "versions": [
      {
        "finalized": true,
        "createdAt": "1788652800000",
        "lastUpdated": "1788652800000",
        "duplicatesAllowed": true,
        "reuseForNonIndexed": true,
        "stateFunctionPreset": "Stateless",
        "publicParamsSchema": [
          { "key": "ownershipRequirements", "label": "Requirements", "type": "ownershipRequirements", "required": true }
        ],
        "verificationCall": {
          "uri": "https://api.bitbadges.io/api/v0/integrations/query/must-own-badges",
          "passAddress": true
        }
      }
    ]
  }
}
{
  "plugin": {
    "_docId": "must-own-badges",
    "pluginId": "must-own-badges",
    "createdBy": "",
    "managedBy": "",
    "metadata": {
      "name": "Token Requirements",
      "description": "Which tokens must the user own?",
      "image": "https://bitbadges.io/images/bitbadgeslogo.png",
      "createdBy": "BitBadges",
      "documentation": "https://docs.bitbadges.io",
      "sourceCode": ""
    },
    "locale": "en",
    "toPublish": false,
    "reviewCompleted": true,
    "lastUpdated": "1788652800000",
    "createdAt": "1788652800000",
    "versions": [
      {
        "version": "0",
        "finalized": true,
        "createdAt": "1788652800000",
        "lastUpdated": "1788652800000",
        "requiresSessions": false,
        "requiresUserInputs": false,
        "duplicatesAllowed": true,
        "reuseForNonIndexed": true,
        "receiveStatusWebhook": false,
        "stateFunctionPreset": "Stateless",
        "userInputsSchema": [],
        "privateParamsSchema": [],
        "publicParamsSchema": [
          { "key": "ownershipRequirements", "label": "Requirements", "type": "ownershipRequirements", "required": true }
        ],
        "verificationCall": {
          "uri": "https://api.bitbadges.io/api/v0/integrations/query/must-own-badges",
          "passAddress": true,
          "hardcodedInputs": []
        }
      }
    ]
  }
}

getPluginErrors returns the stored failures for a plugin you own:

json
{
  "docs": [
    {
      "_docId": "ticket-check-1788739200000",
      "pluginId": "ticket-check",
      "timestamp": 1788739200000,
      "error": "Ticket 4821 was already redeemed",
      "context": { "claimId": "claim_demo_01", "claimAttemptId": "8e1c4a6f2d9b3e7a5c0f1d4b6a8e2c9f", "instanceId": "ticket-gate" }
    }
  ],
  "bookmark": "eyJza2lwIjoyNX0",
  "total": 1
}

The eight core plugins below are not plugin documents, so getPlugin does not return them. Their schemas are fixed and listed here. Every other id is a plugin document that getPlugin describes in full.

Plugin ids are localized. The English document is the bare id (min-badge); other locales append a suffix (min-badge-es).

Core Plugins

Core plugins run in memory inside the API. No HTTP call is made.

Plugin idPurposeDuplicates allowedStateful
numUsesCap total claims and assign claim numbersnoyes
codesOne-time codesyesyes
passwordShared secretyesno
transferTimesTime windowsyesno
initiatedByProof of address (sign in)yesno
whitelistAddress list or dynamic store gateyesyes
haltPause the claimyesno
anonymousAllow claims with no addressnono

numUses

Limits total successful claims. This plugin is always required and cannot be made optional through success logic. It is the default claim number assigner: the claim number is the prior numUses count (zero-based).

ts
// Public params
type NumUsesPublicParams = { maxUses: number; hideCurrentState?: boolean; displayAsUnlimited?: boolean }

// Public state
type NumUsesPublicState = { numUses?: number; usedClaimNumbers?: UintRange[]; claimedUsers?: { [bitbadgesAddress: string]: number[] } }

No user input. Fails with Overall max uses exceeded once numUses >= maxUses. claimedUsers is populated when you fetch the claim with fetchAllClaimedUsers: true.

codes

Codes are either generated from seedCode or listed explicitly. Generated code i is sha256("<seedCode>-<i>") + "-<i>". When seedCode is set, the index after the dash is checked against the regenerated hash. When only codes is set, codes.length must equal numCodes.

ts
// User input
type CodesUserInput = { code: string }

// Public params
type CodesPublicParams = { numCodes: number; hideCurrentState?: boolean }

// Private params
type CodesPrivateParams = { codes: string[]; seedCode: string }

// Public state
type CodesPublicState = { usedCodeRanges?: UintRange[] }   // ranges of used code indices

When this plugin is the claim number assigner, the claim number is the code index. Errors: Invalid code in body provided., Invalid code. Not found in list of codes., Code already used.

password

ts
// User input
type PasswordUserInput = { password: string }

// Private params
type PasswordPrivateParams = { password: string }

No public params or state. The comparison is constant-time. Error: Incorrect password.

transferTimes

ts
// Public params
type TransferTimesPublicParams = { transferTimes: UintRange[] }  // allowed windows, UNIX ms

No user input, state, or private params. Passes when Date.now() falls inside a range. Error: We are currently outside the approved time window.

initiatedBy

Requires the claiming address to be signed in to BitBadges, or approved with the Complete Claims scope. No configurable params. Configuration happens in the claim builder UI. Errors: Must be authenticated to claim, Invalid address. Provided address does not match the address of the signed in user.

whitelist

Gates on an address list. The list can be public (in publicParams), private (in privateParams), or a dynamic store. Both 0x and bb1 forms of an address match.

ts
// Public params
type WhitelistPublicParams = { listId?: string; list?: AddressList; maxUsesPerAddress?: number; hasPrivateList?: boolean }

// Private params
type WhitelistPrivateParams = { useDynamicStore?: boolean; dynamicDataId?: string; dataSecret?: string; listId?: string; list?: AddressList }

// Private state
type WhitelistPrivateState = { addresses: { [address: string]: number } }  // claims per address

A whitelist: false list acts as a denylist. When this plugin is the claim number assigner, the claim number is the address's index in the list. A public dynamic store (publicUseInClaims) needs no dataSecret for reads; other stores do. See Dynamic Stores. Errors: User not in whitelist, User in denylist, User already exceeded max uses, Dynamic data doc not found, Invalid data secret.

halt

No params. Every attempt fails with Claim halted while the plugin is present. Add it to pause, remove it to resume.

anonymous

No params. Requires the claiming address to be the null address bb1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqs7gvmv. Error: BitBadges address must be anonymous.

BitBadges-Hosted Plugins

These are plugin documents created by BitBadges. Their verificationCall.uri points at https://api.bitbadges.io/api/v0/integrations/query/<id>, and the API handles them in-process rather than over the network. All are Stateless, finalized, and allow duplicates.

Plugin idChecksPublic paramsUser inputspassAddressOn-demand compatible
must-own-badgesOwnership requirements ($and / $or groups of assets, amounts, times)ownershipRequirements (type ownershipRequirements, required)noneyesyes
min-badgeBADGE balance of at least minBalance (whole BADGE, ubadge / 1e9)minBalance (number, required)noneyesyes
satisfies-claimThe address has at least one success on another claim. Indexed claims check history; on-demand claims re-run the criteria.claimIdToCheck (string, required)noneyesyes
username-setThe address has set a BitBadges usernamenonenoneyesyes
url-clickerShows a link the user must open before claiming. The check always passes.url (url, required, rendered as a hyperlink)none (requires user interaction)nono
custom-instructionsShows instructions to the user. The check always passes.instructions (string, required)none (requires user interaction)nono

must-own-badges example public params:

json
{
  "ownershipRequirements": {
    "$and": [
      {
        "assets": [
          {
            "chain": "BitBadges",
            "collectionId": "1",
            "assetIds": [{ "start": "1", "end": "20" }],
            "ownershipTimes": [],
            "mustOwnAmounts": { "start": "1", "end": "1" }
          }
        ],
        "options": {}
      }
    ]
  }
}

Nested satisfies-claim checks on on-demand claims have a depth limit of 5. Circular references are rejected. Results for min-badge are cached briefly (60 seconds on success, 10 seconds on failure) so a simulation and the real attempt share one check.

Social Account Plugins

Plugin idChecks
discordThe user's connected Discord account
githubThe user's connected GitHub account
github-contributionsThe user's GitHub contributions to a repository
googleThe user's connected Google account
twitchThe user's connected Twitch account
twitterThe user's connected X (Twitter) account

discord, github, google, twitch, and twitter share one shape. The creator lists allowed accounts; the user must be signed in to that provider on BitBadges (the site's Connections flow).

ts
// Public params
type SocialPublicParams = { users: string[]; maxUsesPerUser: number; hasPrivateList: boolean }

// Private params
type SocialPrivateParams = { usernames: string[] }   // the allowed list when hasPrivateList is true

// State (per instance)
type SocialState = { ids: { [id: string]: number }; usernames: { [username: string]: number } }

github-contributions is Stateless with verificationCall.uri https://api.bitbadges.io/api/v0/integrations/query/github-contributions and no creator params in source. Fetch its current schema with getPlugin('github-contributions').

These six run inside the BitBadges service with the user's OAuth session. Their handlers are not part of the open-source indexer repository, so this page documents only the parameter shapes found in source. Use getPlugin for the authoritative schema.

Custom Plugin Contract

A custom plugin is an HTTPS endpoint you register in the developer portal (Plugins tab). BitBadges calls it during simulation and execution, and optionally after the claim resolves. The endpoint answers 200 to pass or any other status to fail.

Request

BitBadges always sends POST with a JSON body. Fields from every source are merged into one flat object. The example is a plugin ticket-check with one user input (ticketNumber), one public param (eventId), one private param (venueSecret), and passAddress: true, running as instance ticket-gate in claim_demo_01:

json
{
  "ticketNumber": "4821",
  "eventId": "demo-launch-2026",
  "venueSecret": "vs_9b2d7a4f1e8c5f3a0e6c9b2d7a4f1e8c",

  "bitbadgesAddress": "bb1py4mfpg6uf59qkyzg0nmau322c5873eeysp5ue",
  "ethAddress": "0x092bb4851ae26850588243e7bef22a56287f4739",
  "isAddressSignedIn": true,

  "pluginSecret": "ps_3f9c1e7a2b8d6f4a0c5e9b2d7a4f1e8c",
  "version": 0,
  "claimId": "claim_demo_01",
  "claimAttemptId": "3b9d2f7a1c4e6b8d0f2a4c6e8b1d3f5a",
  "instanceId": "ticket-gate",
  "_isSimulation": false,
  "_attemptStatus": "executing",
  "lastUpdated": 1788652800000,
  "createdAt": 1788652800000,
  "locale": "en"
}
FieldSourceDescription
ticketNumber, eventId, venueSecretyour schemasuserInputsSchema, publicParamsSchema, privateParamsSchema, and hardcodedInputs, flattened into the body (or sent as headers when headerField is set).
bitbadgesAddress, ethAddress, isAddressSignedInidentityOnly when verificationCall.passAddress is true; otherwise null. ethAddress is derived from bitbadgesAddress when the user has no ETH wallet. isAddressSignedIn is true when the claiming address is signed in.
pluginSecretcontextVerify BitBadges is the caller.
versioncontextPlugin version in use.
claimId, claimAttemptId, instanceIdcontextThe claim, the attempt (empty for simulations), and this plugin instance in the claim.
_isSimulationcontexttrue for a dry run.
_attemptStatuscontextexecuting during the claim, success or failure on the status webhook.
lastUpdated, createdAtcontextClaim last updated and created, UNIX ms.
localecontextThe claiming user's locale.

Headers:

HeaderValue
X-BitBadges-SignatureHMAC-SHA256 hex of `${timestamp}.${JSON.stringify(body)}` keyed with your pluginSecret
X-BitBadges-TimestampThe timestamp used in the signature (UNIX ms as a string)
X-BitBadges-Version1
any schema field with headerField: trueSent as a header instead of a body field

Reserved keys that your schemas cannot use: pluginSecret, version, claimId, bitbadgesAddress, claimAttemptId, _isSimulation, _isQueueHandler, _attemptStatus, lastUpdated, createdAt, email, isAddressSignedIn, locale, instanceId, X-BitBadges-Signature, X-BitBadges-Timestamp, X-BitBadges-Version.

Constraints on the endpoint:

  • Public HTTPS URL that passes SSRF validation (no private or internal addresses). DNS is pinned for the request.
  • Calls to api.bitbadges.io are blocked to prevent loops.
  • Respond within 10 seconds.
  • Handle CORS and method yourself. The caller is a server, not a browser.

Identifying the User

The address is only trustworthy when isAddressSignedIn is true, which requires the claim (or your plugin's requireSignIn) to demand sign in. If you identify users your own way, or do not want to trust BitBadges, issue a one-time code from your service while the user is authenticated with you, collect it as a user input, and verify it in the handler.

Response

Return 200 and a JSON body that matches the version's stateFunctionPreset:

PresetBodyMeaning
Stateless{}Pass. Nothing else is read.
ClaimToken{ "claimToken": "ticket-4821" }A one-time token you issued. BitBadges marks it used only if the whole claim succeeds; a failed claim leaves it available for retry.
ClaimNumbers{ "claimNumber": 0 }The zero-based claim number to assign. Only one plugin per claim can assign claim numbers.
CustomResponseHandleryour shapeYou manage state and interpretation yourself.

A 200 is a hypothetical state transition. The claim can still fail on another plugin. Do not commit external state on 200 alone (see state below).

Returned JSON keys must not contain . (write bob@abc[dot]com, not [email protected]). This is a storage constraint.

Errors

Return a non-200 status with a message field:

json
{ "message": "Ticket 4821 was already redeemed" }

BitBadges stores the error for debugging (getPluginErrors) and may show it to the claiming user or the creator. Be informative without revealing secrets or internal details.

Simulations

BitBadges simulates before execution. Detect a dry run with _isSimulation === true (and an empty claimAttemptId). Validate inputs and preconditions, return the status you would return for real, and do not mutate state, send notifications, or consume tokens. Set ignoreSimulations: true in the version config to skip the simulation call entirely; the plugin then auto-passes simulation and is only called at execution.

For a custom plugin, the execution step trusts the simulation result and replays it through the state handler. Your endpoint is called once per attempt, not twice.

Status Webhook

With receiveStatusWebhook: true, BitBadges POSTs the same payload to your endpoint after the claim resolves with _attemptStatus: 'success' or 'failure'. Retries use exponential backoff: base delay 1 hour, 2^retries * base, maximum 7 days (1h, 2h, 4h, 8h, 16h, and so on). Make the handler idempotent and deduplicate on claimAttemptId.

State Rules

  • All plugins in a claim run in parallel against the state as it was before the attempt. A plugin cannot depend on another plugin's state change in the same attempt.
  • Attempts process through a queue. Live claim totals may be stale by the time your handler runs. Your own params and the context fields are safe to depend on.
  • Prefer BitBadges-managed state (Stateless, ClaimToken, ClaimNumbers). Updates commit only when your plugin returned 200 and the whole claim succeeded.
  • For self-managed state, verify the outcome first with getClaimAttemptStatus(claimAttemptId) or the status webhook, and use idempotency keys because attempts run concurrently.

On-Demand Compatibility

To work with on-demand (non-indexed) claims, set reuseForNonIndexed: true. The plugin must be stateless, take no user inputs, and work from the address and hardcoded params alone.

Plugin Version Config

ts
interface PluginVersionConfig {
  version: number;                 // auto-incrementing
  finalized: boolean;              // immutable and usable by others once true
  stateFunctionPreset: 'Stateless' | 'ClaimToken' | 'ClaimNumbers' | 'CustomResponseHandler';

  duplicatesAllowed: boolean;
  requiresSessions: boolean;
  requiresUserInputs: boolean;
  reuseForNonIndexed: boolean;
  receiveStatusWebhook: boolean;
  skipProcessingWebhook?: boolean;
  ignoreSimulations?: boolean;
  requireSignIn?: boolean;

  userInputsSchema: JsonBodyInputSchema[];
  publicParamsSchema: JsonBodyInputSchema[];
  privateParamsSchema: JsonBodyInputSchema[];

  verificationCall?: {
    uri: string;
    hardcodedInputs: JsonBodyInputWithValue[];
    passAddress?: boolean;
  };

  customDetailsDisplay?: string;   // "Requires {{minBalance}} tokens"
}
FieldDescription
finalizedLocks schemas, URL, and settings. Unfinalized versions are usable only by the plugin creator (Claim Tester).
duplicatesAllowedA claim may include several instances of this plugin with different params.
requiresSessionsThe claim needs an active BitBadges session.
requiresUserInputsThe user must fill a form at claim time. BitBadges renders it from userInputsSchema.
reuseForNonIndexedCompatible with on-demand claims. Requires stateless, no user inputs.
receiveStatusWebhookReceive the final outcome POST described above.
skipProcessingWebhookAuto-pass without calling the endpoint. Used by display-only plugins such as custom-instructions.
ignoreSimulationsDo not call the endpoint during dry runs.
requireSignInThe user must be signed in before this plugin runs.
verificationCall.uriYour handler URL.
verificationCall.hardcodedInputsStatic key-value pairs included in every request (body or header per headerField). Useful for API keys.
verificationCall.passAddressInclude bitbadgesAddress, ethAddress, and isAddressSignedIn in the payload.
customDetailsDisplayTemplate shown to users in the claim UI. {{key}} references public param keys.

Schema Fields

Each entry in a schema array describes one input:

ts
interface JsonBodyInputSchema {
  key: string;                   // field name in the payload
  label: string;                 // shown to the person filling it
  type: string;                  // 'string' | 'number' | 'boolean' | 'date' | 'url' (date values are UNIX ms)
  required?: boolean;
  defaultValue?: string | number | boolean;
  helper?: string;
  options?: { label: string; value: string | number | boolean }[]; // renders a dropdown
  arrayField?: boolean;          // multiple values
  headerField?: boolean;         // send as an HTTP header instead of a body field
  hideFromDetailsDisplay?: boolean; // public params only: hide from the public view
  hyperlink?: { url: string; showAsGenericView?: boolean };
}

The ticket-check plugin from the request example declares its three inputs like this:

json
{
  "userInputsSchema": [
    { "key": "ticketNumber", "label": "Ticket number", "type": "string", "required": true, "helper": "Printed on your ticket" }
  ],
  "publicParamsSchema": [
    { "key": "eventId", "label": "Event", "type": "string", "required": true }
  ],
  "privateParamsSchema": [
    { "key": "venueSecret", "label": "Venue secret", "type": "string", "required": true }
  ]
}

BitBadges-created plugins also use the type ownershipRequirements. Custom plugins use the five listed types.

Versioning: a new version starts unfinalized. Finalize it to make it immutable and usable by others. A claim keeps the version it was created with, even after you publish newer ones. Your handler receives version and createdAt in every request to branch on if needed. Private plugins (default) can be added only by you; published plugins appear in the directory for everyone.

Internal Plugin Interface

Core plugins implement the same shape internally. Custom plugins get the same atomicity: toSet updates apply only if the whole claim succeeds.

ts
interface BackendIntegrationPlugin<P extends ClaimIntegrationPluginType> {
  pluginId: P;
  metadata: {
    name: string;
    description: string;
    image: string;
    createdBy: string;
    stateless: boolean;
    scoped: boolean;             // state scoped to the claim instance
    duplicatesAllowed: boolean;
    mandatoryToSucceed?: boolean; // cannot be made optional via success logic
    toSkipIfSuccessfulSimulation: boolean;
  };
  validate: (args: {
    context: ContextInfo & { instanceId: string; pluginId: string; version: string };
    publicParams: PublicParamsType;
    privateParams: PrivateParamsType;
    customBody?: UserInputType;
    priorState?: any;
    adminInfo?: any;
    simulationResult?: CachedPluginResult;
  }) => Promise<{
    success: boolean;
    error?: string;
    toSet?: object[];       // state updates, applied only if the claim succeeds
    data?: any;
    claimNumber?: number;   // when this plugin assigns claim numbers
    apiCall?: { uri: string; method: string; body: object; headers: object };
  }>;
  defaultState: any;
  getPublicState: (currState: any) => PublicStateType;
  getBlankPublicState: () => PublicStateType;
}

interface ContextInfo {
  locale: string;
  bitbadgesAddress: string;
  ethAddress: string;
  isAddressSignedIn: boolean;
  claimId: string;
  _isQueueHandler: boolean;   // internal: processing from the queue
  _isSimulation: boolean;
  _attemptStatus: string;
  lastUpdated: number;
  createdAt: number;
  claimAttemptId: string;
  assignMethod: string;       // claim number assignment method
  isClaimNumberAssigner: boolean;
}

Edit this page on GitHub