GuidePartner Webhooks

Partner Webhooks

How to receive SmartphoneKey events via HTTP webhooks — configuration, authentication, delivery, and retry behavior.

Partner webhooks allow your system to receive real-time notifications when events occur in SmartphoneKey. When a subscribed event fires, SmartphoneKey makes an HTTP POST to your registered endpoint with the event payload.

How It Works

  1. You register a webhook subscription with a target URL and (optionally) the event types you want to receive.
  2. When a matching event fires, SmartphoneKey delivers it to your endpoint as an HTTP POST.
  3. Your endpoint responds with a 2xx status code to acknowledge receipt.
  4. If delivery fails, AWS EventBridge retries automatically.

Behind the scenes, every webhook is backed by an EventBridge rule + API destination provisioned for your partnerId. EventBridge filters events by detail.partnerId == <your orgId> so you only ever see events for your own tenant.

Available Event Types

SmartphoneKey publishes events from two sources:

SourceDescription
spk.apiDomain events from the B2C API (locks, users, hubs, cameras, keys)
spk.iot.shadowAWS IoT shadow updates from connected hub devices

The detail-type of each event matches the PascalCase event name listed below.

Lock events (spk.api)

detail-typeWhen it fires
LockCreatedA new lock has been created in the system
OwnerSetA lock's owner has been assigned or changed
OrganizationSetA lock has been moved to a different organization
KeyAddedA physical key has been added to a lock
KeyRemovedA physical key has been removed from a lock
KeyWriteRequestedA key write to physical NFC hardware was requested
LockOpenRequestedAn unlock was requested for a lock
ResidentAddedA resident has been granted access to a lock
ResidentRemovedA resident's access to a lock has been revoked
LockUuidChangedA lock's UUID has been replaced

User events (spk.api)

detail-typeWhen it fires
UserCreatedA new user has been registered
ProfileUpdatedA user's profile fields have changed
PhysicalKeyAddedA physical key has been issued to a user
PhysicalKeyRemovedA physical key has been removed from a user
WalletKeyPreparedA wallet key (Apple/Google) is being provisioned
WalletKeyCommittedA wallet key has been issued and the pass is available
WalletKeyRolledBackA wallet key provisioning attempt was rolled back
WalletKeyRemovedA wallet key has been removed from a user
WalletKeyAddedA wallet key was added (legacy single-step path)
AccessibleLockAddedA user has been granted access to a lock
AccessibleLockRemovedA user has lost access to a lock
UserSuspendedA user has been suspended
UserReactivatedA previously suspended user has been reactivated
LicensePlateAddedA license plate has been registered to a user
LicensePlateRemovedA license plate has been removed from a user
DeviceAddedA device has been linked to a user
DeviceRemovedA device has been unlinked from a user

Hub events (spk.api)

detail-typeWhen it fires
HubProvisionedA hub has been provisioned (manufactured / known to the system)
HubClaimedA hub has been claimed by an organization + site
HubReClaimedA hub has been moved from one org/site to another
HubRoleSetA hub's primary/secondary role has been set
HubSuspendedA hub has been suspended
HubDecommissionedA hub has been decommissioned
MigrationStartedA hub-to-hub device migration has started
DeviceMigrationStatusUpdatedA device's migration status has changed
MigrationCompletedA hub-to-hub device migration has finished

Camera events (spk.api)

detail-typeWhen it fires
CameraRegisteredA camera has been registered
CameraUpdatedA camera's metadata or stream URLs have changed
CameraDisabledA camera has been disabled
CameraDeletedA camera has been deleted
CameraLinkedToLockA camera has been associated with a lock
CameraUnlinkedFromLockA camera has been disassociated from a lock
CameraRoleCreatedA 3dEye access role has been created
CameraRoleDeletedA 3dEye access role has been deleted
CameraRoleUserAssignedA user has been added to a camera role
CameraRoleUserRemovedA user has been removed from a camera role
CameraRoleCameraAssignedA camera has been added to a role
CameraRoleCameraRemovedA camera has been removed from a role

RTSP camera events (spk.api)

detail-typeWhen it fires
RtspCameraRegisteredAn RTSP camera has been registered
RtspCameraDeletedAn RTSP camera has been deleted

Temporary key events (spk.api)

detail-typeWhen it fires
TempKeyCreatedA temporary key has been issued
TempKeyUsedA temporary key has been used to unlock
TempKeyUpdatedA temporary key's validity window or rules changed
TempKeyDeletedA temporary key has been deleted

Matter device events (spk.api)

detail-typeWhen it fires
MatterDeviceCreatedA Matter device has been registered
ReportedShadowUpdatedA device reported new shadow state
DesiredShadowUpdatedA new desired shadow state was pushed

Note: ReportedShadowUpdated and DesiredShadowUpdated can be named in a subscription, but they are not re-published to partner webhooks — they are deduplicated against the IoT shadow path. To track device shadow state, subscribe to Shadow Update instead. Of the Matter events, only MatterDeviceCreated is delivered.

IoT events (spk.iot.shadow)

detail-typeWhen it fires
Shadow UpdateAn IoT hub device updated its AWS IoT Device Shadow

See the Event Catalog for payload shapes of the highest-value events.

Webhook Payload

Your endpoint receives the full EventBridge event as a JSON body. The detail object contains the event-specific data; for spk.api events the actual domain event lives under detail.data.

{
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "source": "spk.api",
  "detail-type": "ResidentAdded",
  "time": "2026-04-30T10:30:00Z",
  "region": "eu-west-1",
  "account": "123456789012",
  "version": "0",
  "resources": [],
  "detail": {
    "partnerId": "acme-corp",
    "aggregateType": "lock",
    "aggregateId": "550e8400-e29b-41d4-a716-446655440000",
    "version": 7,
    "timestamp": 1745922600123,
    "data": {
      "type": "ResidentAdded",
      "lockId": 4321,
      "uuid": "550e8400-e29b-41d4-a716-446655440000",
      "userId": "9b3e1d5e-7c6a-4d4d-8b1a-2e4f6a8c0d1e",
      "name": "Jane Resident",
      "email": "resident@example.com",
      "timestamp": "2026-04-30T10:30:00.123Z"
    }
  }
}

The fields you'll most commonly use:

FieldPurpose
idThe AWS-generated EventBridge envelope ID — use as a deduplication key
sourcespk.api or spk.iot.shadow
detail-typeThe PascalCase event name (e.g. ResidentAdded)
detail.partnerIdYour tenant orgId — already filtered by EventBridge
detail.aggregateType / aggregateIdIdentifies the aggregate that produced the event
detail.timestampProjection time as epoch milliseconds (a number) — distinct from detail.data.timestamp, which is the ISO-8601 domain-event time
detail.dataThe full event payload (see Event Catalog)

IoT Shadow Update Payload

The source is spk.iot.shadow and several detail fields are stringified JSON — your handler must call JSON.parse() on them before accessing nested values.

{
  "source": "spk.iot.shadow",
  "detail-type": "Shadow Update",
  "detail": {
    "partnerId": "<tenant-org-id>",
    "thingName": "<device-uuid>",
    "reportedState": "<stringified JSON of reported state>",
    "desiredState": "<stringified JSON of desired state>",
    "deltaState": "{}",
    "metadata": "<stringified IoT shadow metadata>",
    "version": 42
  }
}

reportedState, desiredState, deltaState, and metadata are stringified JSON embedded inside the outer JSON object. Parse each field individually before accessing its contents.

Partner Self-Service

Partners can manage their own credentials and webhook URL without going through SmartphoneKey support.

Partner Portal

A web UI is available per environment:

EnvironmentURL
Devhttps://partner-portal.spk-dev.workers.dev/
Stagehttps://partner-portal.spk-stage.workers.dev/
Nonprodhttps://partner-portal.spk-nonprod.workers.dev/
Prodhttps://partner-portal.spk-prod.workers.dev/

Partners sign in with Auth0 and can:

  • View their orgId and active API key (revealed once on first issue, masked thereafter)
  • Rotate their API key — instantly revokes the previous one
  • Create or update their webhook URL

See the Partner Portal guide for a walkthrough.

Update Webhook URL via API

If you'd rather automate, you can update your webhook URL with your X-API-Key:

curl -X PATCH "https://admin-api.spk-dev.workers.dev/webhooks/$WEBHOOK_ID" \
  -H "X-API-Key: $YOUR_PARTNER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/webhooks/spk-v2"}'

Notes:

  • Only the url field is mutable through this endpoint — to change the event list or rotate the secret, contact your account manager.
  • HTTPS is required in every environment for partner self-service updates.
  • The webhook status is reset to pending until EventBridge re-verifies the new URL.

Webhook Authentication

So your endpoint can confirm that incoming requests are genuinely from SmartphoneKey, a delivery can carry a static secret that SmartphoneKey includes on every request. This is configured admin-side on the EventBridge connection — contact your account manager to set it up.

Supported Authentication Types

auth_typeDescription
noneNo authentication header is added. This is the default for self-service webhooks.
apiKeyHeaderA static secret is sent in a custom request header of your choosing.
bearerTokenA static token is sent in the Authorization: Bearer <token> header.

Per-request HMAC body signing is not currently available. There is no X-SPK-Signature HMAC of the request body — don't build verification around one. Webhooks created through self-service default to none; to have SmartphoneKey attach a static header or bearer token, ask your account manager to configure apiKeyHeader or bearerToken.

Verifying a static token

When a static header or bearer token is configured, compare the received value against your stored secret using a constant-time comparison to avoid timing attacks:

import hmac

def verify_token(received: str, expected: str) -> bool:
    return hmac.compare_digest(received.encode(), expected.encode())
import { timingSafeEqual } from 'crypto';

function verifyToken(received: string, expected: string): boolean {
  const a = Buffer.from(received);
  const b = Buffer.from(expected);
  return a.length === b.length && timingSafeEqual(a, b);
}

As defense in depth, also confirm detail.partnerId equals your orgId, and only ever serve your webhook endpoint over HTTPS.

Delivery and Retry Policy

SmartphoneKey delivers webhooks via AWS EventBridge API destinations, which retry per their configured policy:

ParameterValue
Delivery modelAt-least-once
RetriesPer the EventBridge API-destination retry policy (subject to change)
Retry backoffExponential
Retry triggers5xx responses and timeouts

Because delivery is at-least-once, design your endpoint to be idempotent — use the id field in the EventBridge envelope as a deduplication key. Events that exhaust their retries are dropped; contact support if you need to recover missed events.

Response Requirements

Your endpoint must:

  • Return HTTP 2xx within the timeout window to acknowledge receipt.
  • 4xx responses are treated as permanent failures and are not retried.
  • 5xx responses or timeouts trigger a retry with exponential backoff.

Returning 2xx immediately and processing asynchronously is recommended for webhooks that trigger heavy work.

Filtering Events

Every webhook subscription is automatically filtered to events whose detail.partnerId equals your orgId. You don't need to do per-event filtering for tenancy.

You may additionally filter by detail-type (event name) at subscription time. Only subscribe to the event types your integration needs — this reduces noise and unnecessary traffic to your endpoint.

Security Best Practices

  • Verify the token when one is configured — If you've had a static header or bearer token set up, check it (constant-time) before processing, and confirm detail.partnerId matches your orgId.
  • Use HTTPS — Only register HTTPS endpoints. HTTP is rejected outside the dev environment.
  • Respond quickly — Return 2xx before doing heavy processing. Queue events for async processing if needed.
  • Be idempotent — The same event may be delivered more than once. Use id from the envelope for deduplication.
  • Rotate secrets periodically — Update your webhook secret regularly and support a short overlap window during rotation.

Debugging Deliveries

If your endpoint is not receiving events:

  1. Confirm your endpoint returns 2xx for a test POST.
  2. Check that your subscription includes the correct event types — pass events: ['*'] to receive every event for your tenant.
  3. Verify your endpoint URL is reachable from the public internet (not localhost or a private network).
  4. Review your auth/token check (if configured) — a 4xx response from your endpoint is treated as a permanent failure and will not be retried. Only 5xx responses and timeouts trigger retries.
  5. Check whether events are firing at all using the Event Catalog to confirm when the events you expect should be generated.

Subscribing to All Events

Pass events: ['*'] when creating a webhook subscription to receive every event tagged with your partnerId. The wildcard collapses to an empty detail-type filter on the underlying EventBridge rule — EventBridge then delivers everything for your tenant and your handler can switch on detail-type to fan out.

{
  "url": "https://your-api.example.com/webhooks/spk",
  "events": ["*"]
}

To narrow later, replace events with an explicit list of PascalCase detail-types from the tables above.

Schema & Code Generation

Download the OpenAPI schema to scaffold your webhook handler:

Download: eventbridge-schemas.yaml

Generate a typed client:

# TypeScript
npx openapi-generator-cli generate -i eventbridge-schemas.yaml -g typescript-fetch -o ./generated

# Python
openapi-generator-cli generate -i eventbridge-schemas.yaml -g python -o ./generated

# Go
openapi-generator-cli generate -i eventbridge-schemas.yaml -g go -o ./generated

The bundled schema currently lags the full event catalog above — regeneration from the b2c-api source is tracked separately. Use the tables in this page as the authoritative list of emitted detail-types in the meantime.