Webhooks
Overview
The webhook system allows clients to configure a single HTTP callback that will be triggered when certain events occur in the BKBN platform. Each client can configure one webhook endpoint with authentication settings. This enables real-time notifications and integration with external systems.
Delivery Model
- One callback URL per workspace. Every enabled webhook in the workspace receives every event type, there is no per-event subscription.
- Each request carries an
X-Event-Typeheader equal to the event name, plusContent-Type: application/json. - Your endpoint must respond with a
2xxstatus within 30 seconds. Non-2xxresponses are recorded but not retried. - Be idempotent: a delivery may occasionally be repeated.
- All ID fields are serialized as JSON strings (even numeric platform ids).
- Optional fields are omitted when absent: except
scheduledAtonAPPOINTMENT_DATE_UPDATED, which is always present and may be explicitlynull(null = appointment cleared / reschedule pending).
API Endpoints
Authentication
All webhook management endpoints require JWT authentication. Include your token in the Authorization header:
Authorization: Bearer <your-jwt-token>
Note: This JWT secures the management endpoints (the calls you make to BKBN). It is separate from the authentication BKBN applies when calling your callback URL, see Authentication Types.
Webhook Management
Get Current Webhook Configuration
GET /v1/webhooksReturns the current webhook configuration for the authenticated client.
Response:
{
"id": "123e4567-e89b-12d3-a456-426614174000",
"url": "https://your-domain.com/webhook",
"method": "POST",
"enabled": true,
"authType": "BEARER_TOKEN",
"hasAuthSecret": true,
"createdAt": "2023-01-15T10:30:00Z",
"updatedAt": "2023-01-15T10:30:00Z",
"lastTriggeredAt": "2023-01-15T12:45:00Z"
}Note: The
authSecretfield is never returned in responses for security reasons. ThehasAuthSecretfield indicates whether a secret is configured.
Create/Replace Webhook Configuration
PUT /v1/webhooksCreates or replaces the webhook configuration. Returns 201 when a new webhook is created, 200 when an existing one is replaced.
Request Body:
{
"url": "https://your-domain.com/webhook",
"method": "POST",
"enabled": true,
"authType": "BEARER_TOKEN",
"authSecret": "your-secret-token",
"authHeaderName": "X-Custom-Auth",
"authParameterName": "auth_token"
}Security Notes:
- The
authSecretfield is only used for input and is never returned in responses. - When replacing a webhook, omit
authSecretto keep the existing secret unchanged. - Include
authSecretonly when you want to set a new secret value.
Enable/Disable Webhook
PATCH /v1/webhooksEnables or disables the webhook without removing the configuration.
Request Body:
{
"enabled": false
}Delete Webhook Configuration
DELETE /v1/webhooksPermanently removes the webhook configuration and stops all future deliveries. Returns 204 No Content.
Get Webhook Trigger History
GET /v1/webhooks/triggersReturns the last 10 webhook trigger attempts with details about success/failure.
Response:
[
{
"id": "456e7890-e89b-12d3-a456-426614174001",
"triggeredAt": "2023-01-15T12:45:00Z",
"payload": {
"event": "VISUALS_READY",
"orderId": "12345",
"assignmentId": "12345-A",
"visualType": "POST",
"product": "GROUND_PHOTO",
"realEstatePropertyId": "123e4567-e89b-12d3-a456-426614174000",
"timestamp": "2023-01-15T12:45:00Z"
},
"headers": {
"X-Event-Type": "VISUALS_READY"
},
"statusCode": 200,
"success": true
},
{
"id": "789e1234-e89b-12d3-a456-426614174002",
"triggeredAt": "2023-01-15T11:30:00Z",
"payload": {
"event": "VISUALS_READY",
"orderId": "12346",
"assignmentId": "12346-A",
"visualType": "DOCUMENT",
"product": "FLOOR_PLAN"
},
"statusCode": 500,
"responseBody": "{\"error\": \"Database connection failed\"}",
"errorMessage": "Webhook call failed with status 500 Internal Server Error",
"success": false
}
]Deprecated: The legacy
/webhook,/webhook/,/webhook/enabled, and/webhook/historyendpoints remain available but are deprecated and scheduled for removal on 2026-09-10. Migrate to the/v1/webhooksendpoints above.
Authentication Types
This controls the authentication BKBN applies when calling your callback URL.
NONE
No authentication is applied to webhook requests.
{
"authType": "NONE"
}HEADER
Adds a custom header with the secret value. If authHeaderName is omitted, it defaults to X-API-Key.
{
"authType": "HEADER",
"authSecret": "your-secret",
"authHeaderName": "X-API-Key"
}Result: X-API-Key: your-secret
QUERY_PARAMETER
Adds the secret as a query parameter.
{
"authType": "QUERY_PARAMETER",
"authSecret": "your-secret",
"authParameterName": "api_key"
}Result: ?api_key=your-secret
BEARER_TOKEN
Adds an Authorization header with a Bearer token.
{
"authType": "BEARER_TOKEN",
"authSecret": "your-token"
}Result: Authorization: Bearer your-token
Webhook Events
The following events may be delivered to your callback URL. Every enabled webhook receives all of them; switch on the event field (or the X-Event-Type header) to handle the ones you care about.
| Event | Triggered when |
|---|---|
VISUALS_READY | Materials (visuals, documents) are ready for download |
PHOTOGRAPHER_BOOKED | A photographer (creative) has accepted an assignment |
ORDER_STATUS_UPDATED | An order transitions between statuses |
ASSIGNMENT_STATUS_UPDATED | An assignment transitions between statuses |
APPOINTMENT_DATE_UPDATED | An assignment's shooting appointment date changes |
ESTIMATED_DELIVERY_UPDATED | An assignment's estimated delivery date changes |
MARKETING_MATERIALS_READY | Marketing materials for a property are ready |
Note on timestamps: Delivery events (
VISUALS_READY,PHOTOGRAPHER_BOOKED,MARKETING_MATERIALS_READY) carry atimestamp(when the webhook was sent). State-change events carryoccurredAt(when the change happened on the platform).
Visuals Ready
Triggered when materials (visuals, documents, or other deliverables) are ready for download.
{
"event": "VISUALS_READY",
"timestamp": "2023-01-15T12:45:00Z",
"orderId": "12345",
"assignmentId": "12345-A",
"visualType": "POST",
"product": "GROUND_PHOTO",
"realEstatePropertyId": "RE-123",
"objectReferenceId": "CRM-ABC-987"
}When is the VISUALS_READY event triggered?
VISUALS_READY event triggered?The event is triggered at different points in the order flow, depending on the product and workflow scenario. For the full scenario and product mapping, see Visuals Ready Event Scenarios.
Payload Fields:
event: "VISUALS_READY"timestamp: ISO 8601 timestamp when the webhook was triggeredorderId: Unique identifier of the orderassignmentId: Unique identifier of the assignment within the ordervisualType: Type of visual/resource that's ready (POST, POST_WEB, DOCUMENT, RAW)product: Type of output/deliverable that's ready (e.g., GROUND_PHOTO, FLOOR_PLAN)realEstatePropertyId(optional): identifier of the real estate property (if applicable)objectReferenceId(optional): Your CRM's reference id for the property, if provided
Next Steps:
When you receive this webhook, you should call the Materials API to retrieve the actual materials:
GET /materials/{orderId}/{assignmentId}?product={product}&visualType={visualType}For detailed information about requesting and processing materials, see the Materials API Documentation.
Photographer Booked
Triggered when a photographer (creative) has accepted an assignment.
{
"event": "PHOTOGRAPHER_BOOKED",
"timestamp": "2023-01-15T12:45:00Z",
"orderId": "100234",
"photographerId": "55021",
"assignmentId": "a1b2c3d4"
}Payload Fields:
event:"PHOTOGRAPHER_BOOKED"timestamp: ISO 8601 timestamp when the webhook was triggeredorderId: Unique identifier of the orderphotographerId: Public id of the photographer (creative) who acceptedassignmentId(optional): Identifier of the assignment, when available
Next Steps:
Fetch the current assignment detail (photographer contact, schedule, products) via:
GET /v1/assignments/{assignmentId}Order Status Updated
Triggered when an order transitions between statuses.
{
"event": "ORDER_STATUS_UPDATED",
"occurredAt": "2026-06-25T09:30:00Z",
"orderId": "100234",
"previousStatus": "PLACED",
"newStatus": "IN_PROGRESS"
}Payload Fields:
event:"ORDER_STATUS_UPDATED"occurredAt: ISO 8601 timestamp when the change occurred on the platformorderId: Unique identifier of the orderpreviousStatus: Order status before the changenewStatus: Order status after the change
Order statuses: PLACED, IN_PROGRESS, COMPLETED, CANCELLED (UNKNOWN is a forward-compatible fallback for a status this API version does not yet recognise).
Next Steps:
Fetch the latest order detail via:
GET /v1/orders/{orderId}Assignment Status Updated
Triggered when an assignment transitions between statuses. May also be sent as a same-status nudge carrying refreshed logistics fields.
{
"event": "ASSIGNMENT_STATUS_UPDATED",
"occurredAt": "2026-06-25T09:30:00Z",
"orderId": "100234",
"assignmentId": "a1b2c3d4",
"previousStatus": "SCHEDULED",
"newStatus": "IN_PRODUCTION",
"assignmentType": "GROUND_PHOTO",
"scheduledAt": "2026-06-26T08:00:00Z",
"estimatedDeliveryAt": "2026-06-28",
"actionRequired": null
}Payload Fields:
event:"ASSIGNMENT_STATUS_UPDATED"occurredAt: ISO 8601 timestamp when the change occurred on the platformorderId: Unique identifier of the orderassignmentId: Unique identifier of the assignmentpreviousStatus: Assignment status before the changenewStatus: Assignment status after the changeassignmentType(optional): Assignment type, when availablescheduledAt(optional): Confirmed shooting start, when availableestimatedDeliveryAt(optional): Estimated delivery date (date-only), when availableactionRequired(optional): Why your action is needed, when the status isACTION_REQUIRED(POWER_OF_ATTORNEY_REQUIRED,DOCUMENT_UPLOAD_REQUIRED)
Assignment statuses: CREATED, PHOTOGRAPHER_ASSIGNED, SCHEDULED, IN_PRODUCTION, IN_PROCESS, ACTION_REQUIRED, DELIVERED, CANCELLED, RESCHEDULE_IN_PROCESS (UNKNOWN is a forward-compatible fallback).
Next Steps:
Fetch the latest assignment detail via:
GET /v1/assignments/{assignmentId}Appointment Date Updated
Triggered when an assignment's shooting appointment date changes.
{
"event": "APPOINTMENT_DATE_UPDATED",
"occurredAt": "2026-06-25T09:30:00Z",
"orderId": "100234",
"assignmentId": "a1b2c3d4",
"assignmentType": "GROUND_PHOTO",
"scheduledAt": "2026-06-26T08:00:00Z"
}Payload Fields:
event:"APPOINTMENT_DATE_UPDATED"occurredAt: ISO 8601 timestamp when the change occurred on the platformorderId: Unique identifier of the orderassignmentId: Unique identifier of the assignmentassignmentType(optional): Assignment type, when availablescheduledAt: New confirmed shooting start. Always present;nullmeans the appointment was cleared (reschedule pending).
Estimated Delivery Updated
Triggered when an assignment's estimated delivery date changes.
{
"event": "ESTIMATED_DELIVERY_UPDATED",
"occurredAt": "2026-06-25T09:30:00Z",
"orderId": "100234",
"assignmentId": "a1b2c3d4",
"assignmentType": "GROUND_PHOTO",
"estimatedDeliveryAt": "2026-07-02"
}Payload Fields:
event:"ESTIMATED_DELIVERY_UPDATED"occurredAt: ISO 8601 timestamp when the change occurred on the platformorderId: Unique identifier of the orderassignmentId: Unique identifier of the assignmentassignmentType(optional): Assignment type, when availableestimatedDeliveryAt: Estimated delivery date (date-only)
Marketing Materials Ready
Triggered when marketing materials for a property are ready for a target platform.
{
"event": "MARKETING_MATERIALS_READY",
"timestamp": "2023-01-15T12:45:00Z",
"propertyId": "123e4567-e89b-12d3-a456-426614174000",
"targetPlatform": "INSTAGRAM"
}Payload Fields:
event:"MARKETING_MATERIALS_READY"timestamp: ISO 8601 timestamp when the webhook was triggeredpropertyId: Real estate property idtargetPlatform: Target platform the materials were prepared for (REAL_ESTATE_BROCHURE,INSTAGRAM,LINKEDIN)
Next Steps:
Retrieve the generated materials via:
GET /v1/real-estate-properties/{propertyId}/marketing-materialsOrder & Assignment Status Lifecycle
ORDER_STATUS_UPDATED and ASSIGNMENT_STATUS_UPDATED are sent only when the external status actually changes. Internal progress that maps to the same external status produces no event. A single assignment change can also move the order (for example, the first assignment starting work flips the order from PLACED to IN_PROGRESS), in which case you receive both events.
Order status
stateDiagram-v2
[*] --> PLACED
PLACED --> IN_PROGRESS: first assignment starts work
IN_PROGRESS --> COMPLETED: order finalized
PLACED --> CANCELLED: cancelled
IN_PROGRESS --> CANCELLED: cancelled
COMPLETED --> [*]
CANCELLED --> [*]
| Status | Meaning |
|---|---|
PLACED | Order received; no assignment has started yet. |
IN_PROGRESS | At least one assignment has started (moved past CREATED). |
COMPLETED | All work finished and the order is finalized. Terminal. |
CANCELLED | Order was cancelled. Terminal. |
Assignment status: photo & visual products
stateDiagram-v2
[*] --> CREATED
CREATED --> PHOTOGRAPHER_ASSIGNED: creative accepts
PHOTOGRAPHER_ASSIGNED --> SCHEDULED: shoot date confirmed
SCHEDULED --> IN_PRODUCTION: shoot done, visuals in editing
IN_PRODUCTION --> DELIVERED: visuals sent to client
PHOTOGRAPHER_ASSIGNED --> RESCHEDULE_IN_PROCESS: reschedule, re-selecting creative
SCHEDULED --> RESCHEDULE_IN_PROCESS: reschedule, re-selecting creative
RESCHEDULE_IN_PROCESS --> PHOTOGRAPHER_ASSIGNED: new creative or date arranged
DELIVERED --> [*]
note right of CREATED
Any non-terminal status can become CANCELLED.
DELIVERED and CANCELLED are terminal.
end note
Assignment status: document products
stateDiagram-v2
[*] --> CREATED
CREATED --> ACTION_REQUIRED: your input needed
ACTION_REQUIRED --> CREATED: input provided
CREATED --> IN_PROCESS: documents being processed
IN_PROCESS --> DELIVERED: documents sent to client
DELIVERED --> [*]
note right of ACTION_REQUIRED
actionRequired reason: POWER_OF_ATTORNEY_REQUIRED
or DOCUMENT_UPLOAD_REQUIRED.
end note
| Status | Applies to | Meaning |
|---|---|---|
CREATED | all | Assignment created; work not yet started. |
PHOTOGRAPHER_ASSIGNED | photo/visual | A creative is being scheduled (date/time pending). |
SCHEDULED | photo/visual | Shoot date/time confirmed with the creative. |
IN_PRODUCTION | photo/visual | Shoot done; visuals being edited. |
RESCHEDULE_IN_PROCESS | photo/visual | A reschedule is underway; a new creative or date is being arranged. |
ACTION_REQUIRED | document | We need input from you (see actionRequired). |
IN_PROCESS | document | Documents are being processed. |
DELIVERED | all | Deliverables sent to the client. Terminal. |
CANCELLED | all | Assignment was cancelled. Terminal. |
UNKNOWN is a forward-compatible fallback for either enum, in case a future status is not yet recognised by this API version.
Webhook Headers
All webhook requests include the following headers:
Content-Type: application/jsonX-Event-Type: <event-type>- The type of event that triggered the webhook- Additional headers specific to the event (e.g.,
X-Order-ID,X-User-ID)
Response Handling
Your webhook endpoint should:
- Respond quickly - Return a response within 30 seconds
- Return success status - HTTP 200 or 201 indicates successful processing
- Handle retries - Be idempotent as webhooks may be retried
- Validate the payload - Verify the request is legitimate
Expected Response
HTTP/1.1 200 OK
Content-Type: application/json
{
"status": "success",
"message": "Webhook processed successfully"
}Error Handling
If your webhook endpoint returns an error status (4xx or 5xx), the system will:
- Log the error with response details
- Not retry the webhook call automatically
- Update the
lastTriggeredAttimestamp regardless of success/failure
Integration Examples
Node.js/Express
app.post('/webhook', (req, res) => {
const event = req.body.event; // or read the X-Event-Type header
const { orderId, assignmentId } = req.body;
console.log(`Received webhook: ${event} for order ${orderId}`);
switch (event) {
case 'VISUALS_READY':
requestMaterials(orderId, assignmentId, req.body.product);
break;
case 'PHOTOGRAPHER_BOOKED':
case 'ASSIGNMENT_STATUS_UPDATED':
case 'APPOINTMENT_DATE_UPDATED':
case 'ESTIMATED_DELIVERY_UPDATED':
fetchAssignmentDetail(assignmentId);
break;
case 'ORDER_STATUS_UPDATED':
fetchOrderDetail(orderId);
break;
case 'MARKETING_MATERIALS_READY':
fetchMarketingMaterials(req.body.propertyId);
break;
}
res.json({ status: 'success' });
});
// Called on VISUALS_READY to fetch the actual deliverables.
async function requestMaterials(orderId, assignmentId, product) {
// visualQuality (HD | WEB) is optional and only applies to photo products; HD is the default.
const response = await fetch(
`https://sync.bkbn.com/v1/orders/${orderId}/assignments/${assignmentId}/materials?product=${product}`,
{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
}
);
if (response.ok) {
const materials = await response.json();
console.log('Materials received:', materials);
// Process the materials
}
}Python/Flask
@app.route('/webhook', methods=['POST'])
def handle_webhook():
data = request.get_json()
event = data.get('event') # or read the X-Event-Type header
order_id = data.get('orderId')
assignment_id = data.get('assignmentId')
print(f"Received webhook: {event} for order {order_id}")
if event == 'VISUALS_READY':
request_materials(order_id, assignment_id, data.get('product'))
elif event in ('PHOTOGRAPHER_BOOKED', 'ASSIGNMENT_STATUS_UPDATED',
'APPOINTMENT_DATE_UPDATED', 'ESTIMATED_DELIVERY_UPDATED'):
fetch_assignment_detail(assignment_id)
elif event == 'ORDER_STATUS_UPDATED':
fetch_order_detail(order_id)
elif event == 'MARKETING_MATERIALS_READY':
fetch_marketing_materials(data.get('propertyId'))
return {'status': 'success'}
# Called on VISUALS_READY to fetch the actual deliverables.
def request_materials(order_id, assignment_id, product):
# visual_quality (HD | WEB) is optional and only applies to photo products; HD is the default.
response = requests.get(
f'https://sync.bkbn.com/v1/orders/{order_id}/assignments/{assignment_id}/materials',
params={'product': product},
headers={
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
)
if response.status_code == 200:
materials = response.json()
print('Materials received:', materials)
# Process the materialsTesting
You can test your webhook integration using services like:
- webhook.site - Free webhook testing
- ngrok - Local development tunneling
- httpbin.org - HTTP request testing
Security Considerations
- HTTPS Only - Always use HTTPS URLs for production webhooks
- Authentication - Use strong secrets for webhook authentication
- Secret Management - Secrets are never returned in API responses for security
- Validation - Validate webhook payloads in your endpoint
- Rate Limiting - Implement rate limiting on your webhook endpoints
- Logging - Log webhook requests for debugging and monitoring (but never log secrets)
Trigger History
The system automatically tracks the last 10 webhook trigger attempts for debugging and monitoring purposes.
What's Tracked
- Timestamp - When the webhook was triggered
- Payload - The data sent to the webhook
- Headers - Additional headers sent with the request
- Status Code - HTTP response code (for both success and failure)
- Response Body - Response content (limited to 4KB, only stored on failure)
- Error Message - Error details (limited to 1KB, only stored on failure)
- Success Flag - Whether the webhook call succeeded
Automatic Cleanup
- Only the last 10 trigger attempts are kept per webhook
- Older records are automatically deleted when new triggers occur
- History is deleted when the webhook is deleted
Accessing History
Use the GET /webhook/history endpoint to retrieve trigger history for debugging webhook issues.
Troubleshooting
Common Issues
-
Webhook not triggering
- Check if the webhook is enabled
- Verify the URL is accessible
- Check the authentication configuration
-
Authentication failures
- Verify the authentication type and secret
- Check header/parameter names for custom auth
-
Timeout errors
- Ensure your endpoint responds within 30 seconds
- Optimize webhook processing logic
Debug Information
Check the webhook configuration and last triggered timestamp:
GET /v1/webhooksLook for error logs in the service logs for debugging webhook call failures.
Updated about 1 month ago
