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
- You register a webhook subscription with a target URL and (optionally) the event types you want to receive.
- When a matching event fires, SmartphoneKey delivers it to your endpoint as an HTTP POST.
- Your endpoint responds with a
2xxstatus code to acknowledge receipt. - 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:
| Source | Description |
|---|---|
spk.api | Domain events from the B2C API (locks, users, hubs, cameras, keys) |
spk.iot.shadow | AWS 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-type | When it fires |
|---|---|
LockCreated | A new lock has been created in the system |
OwnerSet | A lock's owner has been assigned or changed |
OrganizationSet | A lock has been moved to a different organization |
KeyAdded | A physical key has been added to a lock |
KeyRemoved | A physical key has been removed from a lock |
ResidentAdded | A resident has been granted access to a lock |
ResidentRemoved | A resident's access to a lock has been revoked |
LockUuidChanged | A lock's UUID has been replaced |
LockOpenRequested | An unlock command was delivered to the lock |
LockOpenConfirmed | The lock reported that an unlock was carried out |
LockOpenUnconfirmed | An unlock was sent but the lock did not confirm the bolt moved, or did not answer in time |
KeyWriteRequested | A key write to physical NFC hardware was requested |
LockIvrCallCompleted | An IVR call to a resident about this lock ended (telephony-api). Delivered at least once — deduplicate on callId |
Temporary key events (spk.api)
detail-type | When it fires |
|---|---|
TempKeyCreated | Deprecated: a temporary key has been issued through a per-lock route |
TempKeyUsed | Deprecated: a temporary key has been used to unlock (see TempKeyDoorOpened) |
TempKeyDeleted | Deprecated: a temporary key has been deleted through a per-lock route |
TempKeyUpdated | Deprecated: a temporary key's rules changed through a per-lock route |
TempKeyIssued | A temporary key has been issued for one or more doors |
TempKeyChanged | A temporary key's validity, limit, access or guest instructions changed |
TempKeyRevoked | A temporary key has been revoked; none of its doors open with it any more |
TempKeyDoorOpened | A door has been opened with a temporary key |
User events (spk.api)
detail-type | When it fires |
|---|---|
UserCreated | A new user has been registered |
ProfileUpdated | A user's profile fields have changed |
UserPhoneNumberUpdated | A user's phone number has changed (only the masked number is carried) |
UserEmailUpdated | A user's email address has changed |
PhysicalKeyAdded | A physical key has been issued to a user |
PhysicalKeyRemoved | A physical key has been removed from a user |
WalletKeyPrepared | A wallet key (Apple/Google) is being provisioned |
WalletKeyCommitted | A wallet key has been issued and the pass is available |
WalletKeyRolledBack | A wallet key provisioning attempt was rolled back |
WalletKeyRemoved | A wallet key has been removed from a user |
WalletKeyOrgAssigned | An admin assigned a wallet key to its issuing organization |
AccessibleLockAdded | A user has been granted access to a lock |
AccessibleLockRemoved | A user has lost access to a lock |
UserSuspended | A user has been suspended |
UserReactivated | A previously suspended user has been reactivated |
WalletKeyAdded | A wallet key was added (legacy single-step path) |
LicensePlateAdded | A license plate has been registered to a user |
LicensePlateRemoved | A license plate has been removed from a user |
DeviceAdded | A device has been linked to a user |
DeviceRemoved | A device has been unlinked from a user |
DevicePushMutedUpdated | A user muted or unmuted alarm push notifications for one of their devices |
CallToOpenLockChosen | Locks or Matter devices were chosen to open when the user calls the call-to-open number, each on its own digit; replaces the previous choice whole; a preference, not an access grant |
CallToOpenLockCleared | The user's call-to-open choice was cleared whole by a ClearCallToOpenLock command; removing a chosen lock or device from the user's lists drops that target from the choice, without this event |
CallToOpenCallRecorded | A call from the user's own phone number was admitted to open the user's call-to-open choice (the lock open is requested next and may still fail; LockOpenRequested on the lock is the open itself); written only for an admitted call, never for a refused one, and without the phone number; an admitted call counts towards the user's call-to-open rate limit and repeated-call check even when its open then fails |
UserSubscriptionStatusUpdated | An organization's subscription status entry for a user changed (mobile app access); carries only the changed fields (delivered only to the organization whose entry changed) |
Hub events (spk.api)
detail-type | When it fires |
|---|---|
HubProvisioned | A hub has been provisioned (manufactured / known to the system) |
HubClaimed | A hub has been claimed by an organization and site |
HubReClaimed | A hub has been moved from one organization/site to another |
HubRoleSet | A hub's primary/secondary role has been set |
HubSuspended | A hub has been suspended |
HubDecommissioned | A hub has been decommissioned |
MigrationStarted | A hub-to-hub device migration has started |
DeviceMigrationStatusUpdated | A device's migration status has changed |
MigrationCompleted | A hub-to-hub device migration has finished |
Camera events (spk.api)
detail-type | When it fires |
|---|---|
CameraRegistered | A camera has been registered |
CameraUpdated | A camera's metadata or stream URLs have changed |
CameraDisabled | A camera has been disabled |
CameraDeleted | A camera has been deleted |
CameraLinkedToLock | A camera has been associated with a lock |
CameraUnlinkedFromLock | A camera has been disassociated from a lock |
CameraRoleCreated | A 3dEye access role has been created |
CameraRoleDeleted | A 3dEye access role has been deleted |
CameraRoleUserAssigned | A user has been added to a camera role |
CameraRoleUserRemoved | A user has been removed from a camera role |
CameraRoleCameraAssigned | A camera has been added to a role |
CameraRoleCameraRemoved | A camera has been removed from a role |
RTSP camera events (spk.api)
detail-type | When it fires |
|---|---|
RtspCameraRegistered | An RTSP camera has been registered; placement on a video worker follows |
RtspCameraUpdated | An RTSP camera's name or source has changed |
RtspCameraDeleted | An RTSP camera has been deleted |
RtspCameraAccessGranted | A user has been granted access to an RTSP camera |
RtspCameraAccessRevoked | A user's access to an RTSP camera has been revoked |
RtspCameraStreamStarted | An RTSP camera's stream has been started |
RtspCameraStreamStopped | An RTSP camera's stream has been stopped |
RtspCameraPlaced | An RTSP camera has been placed on a video worker and its stream confirmed |
RtspCameraPlacementFailed | An RTSP camera could not be placed on any video worker |
RtspCameraRecordingConfigured | An RTSP camera's recording retention was set (0 = recording off) |
RtspCameraRecordingsPurgeRequested | A purge of an RTSP camera's older recordings was requested |
RtspCameraRecordingsPurged | A requested recordings purge has finished |
RtspCameraRuntimeStatusChanged | An RTSP camera's runtime status changed, as reported by its video worker (delivered only for organization-owned cameras) |
RtspCameraMonitoringConfigured | An RTSP camera's availability monitoring was switched on or off |
RtspCameraUnavailabilityConfirmed | A monitored RTSP camera is still unavailable; repeated at most every five minutes while it stays so (delivered only for organization-owned cameras) |
Video worker events (spk.api)
detail-type | When it fires |
|---|---|
VideoWorkerStarted | A video worker (VMS box) started up; one event per organization with cameras on it, listing only that organization's cameras |
Matter device events (spk.api)
detail-type | When it fires |
|---|---|
MatterDeviceCreated | A Matter device has been registered |
ReportedShadowUpdated ¹ | A device reported new shadow state |
DesiredShadowUpdated ¹ | A new desired shadow state was pushed |
MatterPinSet | A PIN credential slot was provisioned on a Matter door lock (the PIN itself is never carried) |
MatterPinCleared | A PIN credential slot was cleared on a Matter door lock |
AppPinSendRequested | An app-PIN delivery attempt was sent to the lock |
AppPinShadowWriteFailed | An app-PIN write could not be made to the device shadow; the attempt will be retried |
AppPinAcked | The lock acknowledged an app-PIN write |
AppPinClearRequested | An app-PIN slot clear was sent to the lock |
AppPinGaveUp | The lock refused every app-PIN attempt; delivery stopped |
AppPinRedriven | App-PIN delivery was restarted from the beginning |
AppPinRetired | App-PIN delivery was dropped because the lock does not require a remote PIN |
AppPinDeliveryFailed | The bridge answered an app-PIN write without the lock carrying it out; the attempt will be retried |
MatterDeviceNameSet | The display name of a Matter device has been set |
¹ Not delivered to webhooks. It can be named in a subscription, but it is never published to EventBridge — subscribe to
Shadow Updateto track device shadow state.
Alarm events (spk.api)
detail-type | When it fires |
|---|---|
AlarmTriggered | A Matter sensor (smoke, CO, occupancy, …) reported an alarm condition |
AlarmCleared | A previously triggered alarm condition has cleared |
AlarmModeSet | Alarm pushes of a Matter device were switched on or off |
IoT events (spk.iot.shadow)
detail-type | When it fires |
|---|---|
Shadow Update | An IoT hub device updated its AWS IoT Device Shadow |
See the Event Catalog for the payload of every event.
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:
| Field | Purpose |
|---|---|
id | The AWS-generated EventBridge envelope ID — use as a deduplication key |
source | spk.api or spk.iot.shadow |
detail-type | The PascalCase event name (e.g. ResidentAdded) |
detail.partnerId | Your tenant orgId — already filtered by EventBridge |
detail.aggregateType / aggregateId | Identifies the aggregate that produced the event |
detail.timestamp | Projection time as epoch milliseconds (a number) — distinct from detail.data.timestamp, which is the ISO-8601 domain-event time |
detail.data | The 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:
| Environment | URL |
|---|---|
| Dev | https://partner-portal.spk-dev.workers.dev/ |
| Stage | https://partner-portal.spk-stage.workers.dev/ |
| Nonprod | https://partner-portal.spk-nonprod.workers.dev/ |
| Prod | https://partner-portal.spk-prod.workers.dev/ |
Partners sign in with Auth0 and can:
- View their
orgIdand 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
urlfield 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
pendinguntil 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_type | Description |
|---|---|
none | No authentication header is added. This is the default for self-service webhooks. |
apiKeyHeader | A static secret is sent in a custom request header of your choosing. |
bearerToken | A static token is sent in the Authorization: Bearer <token> header. |
Per-request HMAC body signing is not currently available. There is no
X-SPK-SignatureHMAC of the request body — don't build verification around one. Webhooks created through self-service default tonone; to have SmartphoneKey attach a static header or bearer token, ask your account manager to configureapiKeyHeaderorbearerToken.
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:
| Parameter | Value |
|---|---|
| Delivery model | At-least-once |
| Retries | Per the EventBridge API-destination retry policy (subject to change) |
| Retry backoff | Exponential |
| Retry triggers | 5xx 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
2xxwithin the timeout window to acknowledge receipt. 4xxresponses are treated as permanent failures and are not retried.5xxresponses 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.partnerIdmatches yourorgId. - Use HTTPS — Only register HTTPS endpoints. HTTP is rejected outside the dev environment.
- Respond quickly — Return
2xxbefore doing heavy processing. Queue events for async processing if needed. - Be idempotent — The same event may be delivered more than once. Use
idfrom 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:
- Confirm your endpoint returns
2xxfor a test POST. - Check that your subscription includes the correct event types — pass
events: ['*']to receive every event for your tenant. - Verify your endpoint URL is reachable from the public internet (not localhost or a private network).
- Review your auth/token check (if configured) — a
4xxresponse from your endpoint is treated as a permanent failure and will not be retried. Only5xxresponses and timeouts trigger retries. - 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.