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 |
KeyWriteRequested | A key write to physical NFC hardware was requested |
LockOpenRequested | An unlock was requested for 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 |
User events (spk.api)
detail-type | When it fires |
|---|---|
UserCreated | A new user has been registered |
ProfileUpdated | A user's profile fields have 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 |
WalletKeyAdded | A wallet key was added (legacy single-step path) |
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 |
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 |
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 + site |
HubReClaimed | A hub has been moved from one org/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 |
RtspCameraDeleted | An RTSP camera has been deleted |
Temporary key events (spk.api)
detail-type | When it fires |
|---|---|
TempKeyCreated | A temporary key has been issued |
TempKeyUsed | A temporary key has been used to unlock |
TempKeyUpdated | A temporary key's validity window or rules changed |
TempKeyDeleted | A temporary key has been deleted |
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 |
Note:
ReportedShadowUpdatedandDesiredShadowUpdatedcan 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 toShadow Updateinstead. Of the Matter events, onlyMatterDeviceCreatedis delivered.
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 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:
| 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.