Environment Setup

Set up a development environment against the hosted API: the SDK, a sandbox key, a verified ingest call, and the errors you will meet first.

Last reviewed September 18, 2026Fresh

Overview

This guide sets up a development environment against the live platform. There is no local PasskeyBridge to run: the API is hosted, and you develop against https://api.passkeybridge.io with a sandbox key.

By the end you will have the SDK installed, a sandbox key that is never billed, a verified ingest call, and a clear rule for which key belongs in which environment.

Read Quickstart first if you have not sent a signal yet; this guide assumes you have an organization and can reach the dashboard.

System requirements

RequirementVersion
Node.js20.19 or later, which the SDK declares in its engines field
Package managernpm, pnpm or Bun
TypeScriptOptional. The SDK ships its own type declarations
curlAny recent version, for the checks below
BrowserOne with WebAuthn support, only if you are building passkey flows

Docker is not required and would not help: there is no self-hosted runtime. jq is useful for reading JSON responses in a terminal, and the dashboard's API playground covers exploratory calls without a REST client.

SDK installation

The wallet SDK. @passkeybridge/vc-wallet-sdk covers the credential surface: issuance, verification, presentation, OID4VCI offers, StatusList2021 lifecycle, and a local encrypted wallet.

npm install @passkeybridge/vc-wallet-sdk
import { PBWallet } from "@passkeybridge/vc-wallet-sdk";

const wallet = new PBWallet({
  apiKey: process.env.PB_API_KEY!,     // pb_live_... or pb_test_...
  tenantId: process.env.PB_TENANT_ID!, // your Organization ID
  baseUrl: "https://api.passkeybridge.io/v1", // this is the default
  persist: true,                       // IndexedDB in a browser, memory elsewhere
});

The current release is 0.6.0, Apache-2.0, with no runtime dependencies. baseUrl defaults to https://api.passkeybridge.io/v1, so the option only matters if you are pointed somewhere else. Releases are published from GitHub Actions through npm OIDC trusted publishing; the repository is private, so the package is published without a provenance attestation. Pin to the current minor (^0.6.0): the 0.x line may break between minors.

Scopes the SDK needs: vc_issue for issuance and offers, vc_verify for verification and presentation requests, vc_revoke for revoke, suspend, reinstate and status-list rebuilds. checkStatus, batchCheckStatus and getIssuerMetadata need no key at all.

Sensor capture. captureSensorFingerprint is a source file in the PasskeyBridge dashboard (src/lib/sensor-capture.ts) rather than a published package. To use it, copy the file into your own project from the SDK page. It hashes sensor readings on the device and sends digests.

Without an SDK. Every SDK method maps to one HTTP call. From Python, Go, Ruby or anything else, call the REST API directly with the same headers: x-pb-api-key and x-pb-tenant-id.

Recommended project structure

Nothing here is required by the platform. It is the layout that keeps keys out of the client bundle and makes the sandbox-to-production switch a configuration change.

your-app/
  src/
    lib/pb-client.ts        # thin wrapper around the REST calls
    config/pb-config.ts     # environment-aware configuration
  .env.local                # sandbox key, never committed
  .env.production           # production key, injected by CI
// src/config/pb-config.ts
export const PB_CONFIG = {
  apiBase: process.env.PB_API_BASE ?? "https://api.passkeybridge.io/v1",
  tenantId: process.env.PB_TENANT_ID ?? "",
  apiKey: process.env.PB_API_KEY ?? "",
} as const;

An API key is a bearer credential: it belongs on your server, in your platform's secret store, and never in a bundle a browser downloads. If you are using Vite, remember that a VITE_-prefixed variable is inlined into client code, which makes it the wrong place for a key. Add .env.local and .env.production to .gitignore.

Sandbox keys and scopes

One account has one organization, created when you sign up. There is no second sandbox organization to provision, and the wizard will not create one for an account that already has one. Sandbox is a property of the key, not of a separate environment.

Make a sandbox key. Go to Core > API keys, click Generate API key, tick Sandbox key before generating, and select the scopes you need. The key comes back prefixed pb_test_ and the API keys tab marks it with a Test badge.

Scopes. For development a broad key is convenient; for production, mint one key per job with only the scopes that job needs.

ScopeGrants
ingestPost signals, and the BLAST tunnel lifecycle
eventsRead processed events and metrics
metricsRead organization metrics only
vc_issueIssue credentials and create OID4VCI offers
vc_verifyVerify presentations and build OpenID4VP requests
vc_revokeRevoke, suspend, reinstate, rebuild the status list
spatialAtomic fingerprint binding
scimSCIM 2.0 user provisioning
okta_hooksOkta event hook consumer
dpopDPoP binding, verification and nonce issuance
supply_chainSupply-chain attestation (Enterprise)
passkey_rpHosted passkey relying party
passkey_rp_importBulk passkey credential import
receiptsEntropy provenance receipts (Enterprise)

admin and breakglass are deliberately not mintable: they exist only for console actions taken with a signed-in admin session. Four older names (playbooks, credentials, agents, shadow) were retired in September 2026; keys that still carry one keep working, and the name gates nothing.

The key is shown once. PasskeyBridge stores its SHA-256 digest and the first 16 characters. Copy it into your secret store straight away; a lost key is revoked and replaced, never recovered.

Sandbox vs. production keys

Every key carries a prefix that says which it is: pb_live_ (production) or pb_test_ (sandbox). There is one host and one data store. A sandbox key and a production key hit https://api.passkeybridge.io, write into the same organization, and are told apart by the key row.

DimensionSandbox key (pb_test_)Production key (pb_live_)
Hostapi.passkeybridge.ioThe same
MeteringNever metered, and never counted against your monthly allotmentMetered
Volume ceiling1,000 signals per rolling 30 days, shared with dashboard test mode. Past it: 429 with x-pb-reason: sandbox-capThe plan allotment, then 429 or overage
Rate limitThe plan's per-minute limit, the sameThe same
Outbound actionsSimulated. Webhook, Slack and email actions record skipped_sandbox and send nothingSent
Internal actionsExecuted for real (delegates, tunnels, proofs, shadow identities)The same
Stored eventtest_mode = true, and a Test badge in the Events tabtest_mode = false
Credential issuance, BLAST tunnelsRealReal

Two consequences worth planning around. Sandbox traffic lands beside production traffic in the same Events tab, so filter by Test only or Live only when you are reading it. And because outbound actions are simulated, a sandbox run proves your playbook matched and would have fired; it does not prove your webhook receiver works. Send one live signal to a staging receiver for that.

Dashboard test mode is the other unmetered lane. Send test signal on the Overview tab posts a test_signal with your session token and the x-pb-test-mode: true header instead of a key, which requires you to be an admin of the organization and to have at least one active key. It shares the same 1,000-per-30-days ceiling. The Test mode switch beside it only shows a reminder banner; it does not change what the button sends.

Test mode does not gate everything. Parametric revocation still runs on a hard signal in test mode, because it touches only your own PasskeyBridge rows.

First local test

1. Check the host answers, with no credentials:

curl -s https://api.passkeybridge.io/v1/openapi-spec | jq '.info.title, .info.version'
# "PasskeyBridge™ API"
# "2.1.0"

2. Send a signal with your sandbox key:

curl -X POST https://api.passkeybridge.io/v1/shield-ingest \
  -H "Content-Type: application/json" \
  -H "x-pb-api-key: $PB_API_KEY" \
  -H "x-pb-tenant-id: $PB_TENANT_ID" \
  -d '{
    "event_type": "sim_swap",
    "phone": "+15551234567",
    "risk_score": 0.72
  }'

Expected (200):

{
  "status": "ok",
  "result": "no_matching_playbook",
  "actions_count": 0,
  "actions_failed_count": 0,
  "latency_ms": 118,
  "correlation_id": "pb-19a2f3c4d5e-7b3c9f02"
}

result reads playbook_executed once an active playbook matches sim_swap. status turns to warning if an action failed, with the count in actions_failed_count.

3. Confirm it landed. Open Core > Events. Your signal is at the top of the first page, carrying the Test badge because the key is a sandbox key. Click the row: the drawer shows the decision and response times, the result, the matched playbook id, the correlation id from the response above, the stored metadata, and every action with its status. Any extra field you sent is forwarded to playbook actions and is not stored on the event.

4. The same call from Node:

// test-ingest.ts, run with: npx tsx test-ingest.ts
const res = await fetch("https://api.passkeybridge.io/v1/shield-ingest", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-pb-api-key": process.env.PB_API_KEY!,
    "x-pb-tenant-id": process.env.PB_TENANT_ID!,
  },
  body: JSON.stringify({ event_type: "test_signal", risk_score: 0.5 }),
});
console.log(res.status, await res.json());

Common setup errors

ResponseCauseFix
401 Invalid API keyUnknown, revoked, or belonging to a different organization. The lookup matches key digest and organization together, so another organization's valid key reads as invalidCheck the key is active on Core > API keys and that x-pb-tenant-id is the organization it was minted in
401 Missing x-pb-api-key headerThe header did not arriveCall /v1/_debug/echo and read api_key_present. If it is false, something between you and the edge dropped the header
403 API key missing 'ingest' scope, x-pb-reason: missing-scopeThe key names scopes and not the one this endpoint needsMint a key with the right scope. Scopes cannot be edited after minting
403 Direct access denied.The call went to the Supabase functions host instead of api.passkeybridge.ioSend it to api.passkeybridge.io under the /v1/ prefix
404 Unknown tenantThe organization id is wrongCopy it again from Developer > Settings, field Organization ID
404 x-pb-reason: unknown-functionThe function name is not on the public allowlist, or the /v1/ prefix is missingCheck the spelling against the API reference
422 Payload validation failedA field failed the schema. The details array names each oneUsually a phone_hash that is not 64 hex characters, or a missing event_type
429 with limit and layerOver the per-minute rate limit: Starter 60, Pro 300, Enterprise 600Back off and retry. X-RateLimit-Limit carries the limit that applied
429 x-pb-reason: sandbox-capThe 1,000-per-30-days ceiling on unmetered trafficUse a live key for volume testing, or wait for the window to roll
429 Starter plan monthly signal limit reachedThe monthly allotmentUpgrade, or wait for the next month
CORS error in a browserYou are calling the API from client codeCall it from your server. An API key in a browser is a disclosed credential, whatever CORS says

A 401 or 403 with no x-pb-reason header came from the edge rather than from the function.

CI/CD and deployment

Secrets. Keep keys in your platform's secret store (GitHub Actions secrets, Vercel environment variables, AWS Systems Manager). Never in source, never in a Dockerfile, never in a client bundle.

VariableValue
PB_API_BASEhttps://api.passkeybridge.io/v1
PB_TENANT_IDYour Organization ID
PB_API_KEYThe raw key, pb_live_ in production and pb_test_ everywhere else

Before going live:

  1. Mint a production key with only the scopes that job needs, rather than reusing the broad development key.
  2. Store it in the secret store and confirm nothing reads it from a client-side variable.
  3. Set the Callback URL on Developer > Settings if any playbook uses webhook_callback, and save the Callback signing secret in your receiver so you can verify x-pb-signature.
  4. Activate at least one playbook and exercise it with a sandbox signal, remembering that outbound actions are simulated for a sandbox key.
  5. Send one live signal to a staging receiver to prove the webhook path end to end.
  6. Configure alert rules in the Observability tab on the metrics you care about, such as high_risk_count or error_rate.
  7. Decide how a data subject request will be handled, and read DSAR workflows.

Rotating a key. Generate the new key, update the secret, deploy, then revoke the old one. Both work during the overlap, which is the point; the old key's last used date on the API keys tab tells you when traffic has finished moving.

Rotating the signing secret is different: Rotate on Developer > Settings returns the new secret once and keeps the previous one valid for 24 hours, on both the inbound x-pb-signature check and your own verification of outbound deliveries.

Related from the blog