Quickstart: ingest your first signal

Generate an API key, send a signal to the ingest endpoint, read the response, and make a playbook act on it.

Last reviewed September 18, 2026Fresh

Prerequisites

You need:

  • A PasskeyBridge account. Sign up at app.passkeybridge.io/auth with an email address and a password, or with a passkey.
  • A terminal with curl.

No card is required. The Starter plan includes 100 signals a month; past that, ingest answers 429 until the next month or an upgrade.

Everything below happens against one host, https://api.passkeybridge.io, and inside one organization.

Step 1: Your organization

You do not create one. Signing up creates your organization: a database trigger runs on the new account, takes the Organization Name you typed on the sign-up form (or "My Organization" if you left it blank), and inserts you as its admin.

An organization owns everything else: API keys, playbooks, events, credentials, audit entries. One account has one organization, and the setup wizard offers to create one only if your account somehow has none.

Find your organization id. Open the dashboard, go to Developer > Settings, and copy the value shown under Organization ID. It is a UUID, and it goes in the x-pb-tenant-id header on every API call. The same screen holds the optional Callback URL that webhook_callback actions post to, the Callback signing secret, and the Require signed signals switch.

Step 2: Generate an API key

Go to Core > API keys and click Generate API key. The wizard asks for three things:

  • Key Label, a name for your own use, 1 to 120 characters.
  • Scopes, at least one. For signal ingest pick ingest. events is a read scope and does not permit a write, so a key with events alone is refused by ingest with 403. Fourteen scopes can be minted: ingest, events, metrics, vc_issue, vc_verify, vc_revoke, spatial, scim, okta_hooks, dpop, supply_chain, passkey_rp, passkey_rp_import, receipts.
  • Sandbox key, a checkbox. Leave it off for this walkthrough, so the signal you send behaves exactly like production traffic.

The key is minted on the server and shown once. Copy it immediately: PasskeyBridge stores its SHA-256 digest and the first 16 characters, and nothing can recover the rest. If you lose it, revoke it on the same tab and generate another.

Prefixes tell the two apart. A production key begins pb_live_; a sandbox key begins pb_test_ and carries a Test badge on the API keys tab. Signals from a sandbox key are stored with test_mode = true, are never metered, have their outbound actions simulated rather than sent, and share a ceiling of 1,000 signals per rolling 30 days with dashboard test mode.

Send the key as x-pb-api-key and your organization id as x-pb-tenant-id on every request. SCIM and Okta event hooks are the exception: they take the key in an Authorization: Bearer header.

Optional: send each signal once. Add x-pb-idempotency-key with a value you mint per signal (a UUID is the recommended form; 1 to 128 printable ASCII characters, and Idempotency-Key is accepted as an alias). PasskeyBridge claims the key before any playbook action runs and keeps the outcome for 24 hours. A retry with the same key and the same body gets the stored answer back with x-pb-idempotent-replay: true; if the first attempt ran its actions and then failed to store the event (a 502), the retry stores the event without running the actions again, so a callback, a Slack post or an email is never sent twice for one key. The same key with a different body is refused with 422, and a retry that overlaps the first request answers 409 with a Retry-After header. Without the header, every request is processed as it arrives.

Step 3: Send your first signal

Post the signal to shield-ingest:

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

Body fields. One of event_type or signal_type is required; everything else is optional and has no default.

FieldTypeNotes
event_typestring, 1 to 64 charsLetters, digits, underscore and hyphen only. signal_type is an accepted alias
phonestring, 1 to 32 charsThe raw number. Normalized toward E.164 and keyed-hashed server-side with HMAC-SHA-256 under a server-held pepper. Preferred
phone_hashstring, exactly 64 hex charsA digest you computed. Keyed again under the server pepper before storage and stamped hash_version 2; the callback envelope carries it as you sent it. Ignored when phone is also present
risk_scorenumber, 0 to 1Stored on the event and shown in the dashboard
source_idUUIDYour own source identifier, stored on the event for correlation
credential_sha1string, 40 hex charsRuns a breach-corpus range check and adds the hibp_* fields to the response
subject_refstring, 1 to 128 printable ASCIIYour opaque handle for the subject. Stored in the clear and echoed to your callback, so it is refused if it looks like an email address, a phone number or a long digit run

Any other field is accepted and forwarded to your playbook actions, and is not persisted on the event: the stored metadata holds risk_score, the correlation id and the breach-check fields only.

Neither phone nor phone_hash is required. Without one, the signal is still stored and playbooks still run, and the automatic revocation of agent delegates and A2A negotiations is skipped, because there is no identifier to act on.

The response (200):

{
  "status": "ok",
  "result": "no_matching_playbook",
  "actions_count": 0,
  "actions_failed_count": 0,
  "latency_ms": 96,
  "correlation_id": "pb-19a2f3c4d5e-7b3c9f02"
}
  • status is ok, or warning when at least one action reported an error.
  • result is playbook_executed when an active playbook matched this event type, no_matching_playbook otherwise. On a fresh organization, expect no_matching_playbook; Step 5 fixes that.
  • actions_count counts everything that ran, including the automatic revocation entry and the breach check.
  • correlation_id is worth logging: it appears in the stored event metadata and in the audit log.

Errors and refusals

Every failure comes back as JSON with an error string, and several carry an x-pb-reason header you can branch on.

StatusBody or reasonCause
400Missing tenant ID in path or x-pb-tenant-id headerNo organization id on the request
400Invalid JSON bodyThe body did not parse
400x-pb-reason: invalid-subject-refsubject_ref was not a printable ASCII string of 1 to 128 chars, or looked like an email address or phone number
400x-pb-reason: invalid-idempotency-keyx-pb-idempotency-key was present but not 1 to 128 printable ASCII characters
401Missing x-pb-api-key headerNo key presented
401Invalid API keyUnknown key, revoked key, or a key belonging to another organization
401x-pb-reason: signature-required or invalid-signatureThe organization requires signed ingest and the header was absent or did not verify
402x-pb-reason: payment-requiredA paid plan whose subscription is unpaid, past the Starter allotment
403API key missing 'ingest' scope, x-pb-reason: missing-scopeThe key names scopes and ingest is not among them
403Tenant ID header does not match path tenantThe header and the path segment disagree
403Tenant is suspended.The organization is quarantined
403Direct access denied. Use https://api.passkeybridge.io/v1/{function}.The request reached the functions host directly instead of the public host
404Unknown tenant, x-pb-reason: unknown-tenantNo organization with that id
409x-pb-reason: idempotency-in-flight, with Retry-AfterA request with the same idempotency key is still being processed
404x-pb-reason: unknown-functionThe path is not on the public allowlist. Check the function name and the /v1/ prefix
413Request body too largeOver the small-JSON cap
422Payload validation failed with a details arraySchema failure, one string per problem, for example a phone_hash that is not 64 hex characters
422x-pb-reason: idempotency-key-reusedThe idempotency key was already used with a different request body; mint a new key for a new signal
429Too many requests... with limit and layerOver the per-minute rate limit for your plan
429x-pb-reason: sandbox-capThe 1,000-per-30-days ceiling on sandbox and test-mode traffic
429Starter plan monthly signal limit reached (100).The Starter monthly allotment
502Failed to persist signal eventThe event could not be written, so nothing was metered. With an idempotency key, retry with the same key: the actions already ran and are not run again
503x-pb-reason: tenant-lookup-failedThe organization lookup itself failed. This is deliberately not a 404
503x-pb-reason: idempotency-unavailableThe idempotency store did not answer, so the request was not run; retry with the same key

A 401 or 403 that carries no x-pb-reason at all came from the edge rather than the function, usually a Cloudflare rule. /v1/_debug/echo echoes back which headers arrived, which settles most "my key is not working" questions in one call.

Step 4: Verify in the dashboard

Return to the Dashboard and open the Events tab. The tab lists events newest first, 25 per page, with filters for signal type, playbook result, and live or test traffic. If a signal arrives while you are looking at the first page, a prompt at the top of the list says how many new events there are; click it to load them.

Each event row shows: - Event type—The signal type you sent (e.g., sim_swap). - ResultExecuted when a playbook matched and ran, No match when none was configured for that signal type, Pending while the result is not yet recorded. - Test—Present when the event was sent in test mode or with a sandbox key. - Decision and response timedecision is PasskeyBridge's own path (authenticate, resolve, keyed-hash, match a playbook, decide); response is the whole request including outbound playbook actions. - Timestamp—Server-side ingestion time.

Click a row to open the details drawer: decision path and full response time, the result, the matched playbook id, the correlation id, the stored metadata (risk_score and, when a credential-leak check ran, the HIBP fields), and every action that ran with its status and latency.

Alternative: Send a test signal from the dashboard

If you prefer not to use curl, click Send test signal on the Overview tab. The button needs at least one active API key on the organization; the signal itself is sent with your session (test mode), not with the key.

The dashboard sends a pre-configured signal with: - event_type: test_signal - A random test phone value, keyed-hashed server-side like any other identifier - No risk_score

Test-mode signals use JWT authentication (your session token) instead of an API key. They appear in the Events tab like any other signal, carry the Test badge, and are never metered. The stored event metadata holds risk_score, the correlation id, and HIBP results only; a source field sent with the signal is forwarded to playbook actions but is not stored on the event.

The Test mode switch on the Overview tab shows a banner reminding you that dashboard-sent signals are tagged as test events; it does not change what the button sends.

Alternative: Replay an attack in test mode

The Overview tab also carries Replay an attack in test mode. An organization admin clicks Run replay and PasskeyBridge sends six credential_stuffing_attempt signals, one failed_verification, and one suspicious_login to the organization as test events, then reports which of the organization's own playbooks matched each signal type and the decision-path time (p50 and p95) across the run.

Nothing is metered, nothing is revoked, and nothing leaves PasskeyBridge: the replay uses only soft signal types, runs in test mode, and calls no carrier or customer endpoint. One replay is allowed per organization every ten minutes. Results are saved and the last five appear under the card, so a replay can be compared with the previous one after a playbook change. The events themselves appear in the Events tab with the Test badge.

Step 5: Create a playbook

A signal on its own is a record. A playbook is what makes it do something.

Go to Automation > Playbooks and click New playbook. A playbook has a name, one trigger event type, and one or more actions:

ActionWhat it does
webhook_callbackPOSTs a JSON envelope to your callback URL, signed with x-pb-signature when a signing secret is set. This is the action that lets your own backend act on your own accounts
slack_webhookPosts a message to a Slack destination you registered in the dashboard. The playbook stores a reference; the URL itself is held encrypted
email_alertEmails a group on your organization (admins, or all members). Addresses are resolved when the alert fires
log_eventRecords the signal in the audit trail at a severity you choose
suspend_account, suspend_agentsDeactivate this organization's agent delegates in PasskeyBridge
revoke_tokensTears down this organization's open BLAST tunnels
revoke_sessionsRevokes this organization's active A2A negotiations
freeze_credentialsInvalidates this organization's cached proofs
step_up_authDeactivates this organization's shadow identities
quarantineRevokes cross-references, invalidates cached proofs and deactivates shadow identities together

The internal actions operate on PasskeyBridge's own rows for your organization. None of them reaches your users' accounts, sessions or passkeys; webhook_callback is how you drive those.

How they run. All actions of a matched playbook run in parallel, each raced against an 8 s timeout, and each records its own status and duration. A slow upstream cannot starve the others.

Confirm it. Re-send the same signal from Step 3. The response now reads "result": "playbook_executed" with actions_count at least 1, the Events tab row shows Executed, and the details drawer lists every action with its status. If an action reports an error, status is warning and actions_failed_count is above zero.

Related from the blog