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-Type header equal to the event name, plus Content-Type: application/json.
  • Your endpoint must respond with a 2xx status within 30 seconds. Non-2xx responses 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 scheduledAt on APPOINTMENT_DATE_UPDATED, which is always present and may be explicitly null (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/webhooks

Returns 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 authSecret field is never returned in responses for security reasons. The hasAuthSecret field indicates whether a secret is configured.

Create/Replace Webhook Configuration

PUT /v1/webhooks

Creates 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 authSecret field is only used for input and is never returned in responses.
  • When replacing a webhook, omit authSecret to keep the existing secret unchanged.
  • Include authSecret only when you want to set a new secret value.

Enable/Disable Webhook

PATCH /v1/webhooks

Enables or disables the webhook without removing the configuration.

Request Body:

{
  "enabled": false
}

Delete Webhook Configuration

DELETE /v1/webhooks

Permanently removes the webhook configuration and stops all future deliveries. Returns 204 No Content.

Get Webhook Trigger History

GET /v1/webhooks/triggers

Returns 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/history endpoints remain available but are deprecated and scheduled for removal on 2026-09-10. Migrate to the /v1/webhooks endpoints 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.

EventTriggered when
VISUALS_READYMaterials (visuals, documents) are ready for download
PHOTOGRAPHER_BOOKEDA photographer (creative) has accepted an assignment
ORDER_STATUS_UPDATEDAn order transitions between statuses
ASSIGNMENT_STATUS_UPDATEDAn assignment transitions between statuses
APPOINTMENT_DATE_UPDATEDAn assignment's shooting appointment date changes
ESTIMATED_DELIVERY_UPDATEDAn assignment's estimated delivery date changes
MARKETING_MATERIALS_READYMarketing materials for a property are ready

Note on timestamps: Delivery events (VISUALS_READY, PHOTOGRAPHER_BOOKED, MARKETING_MATERIALS_READY) carry a timestamp (when the webhook was sent). State-change events carry occurredAt (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?

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.

Visuals Ready Event Flow

Payload Fields:

  • event: "VISUALS_READY"
  • timestamp: ISO 8601 timestamp when the webhook was triggered
  • orderId: Unique identifier of the order
  • assignmentId: Unique identifier of the assignment within the order
  • visualType: 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 triggered
  • orderId: Unique identifier of the order
  • photographerId: Public id of the photographer (creative) who accepted
  • assignmentId (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 platform
  • orderId: Unique identifier of the order
  • previousStatus: Order status before the change
  • newStatus: 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 platform
  • orderId: Unique identifier of the order
  • assignmentId: Unique identifier of the assignment
  • previousStatus: Assignment status before the change
  • newStatus: Assignment status after the change
  • assignmentType (optional): Assignment type, when available
  • scheduledAt (optional): Confirmed shooting start, when available
  • estimatedDeliveryAt (optional): Estimated delivery date (date-only), when available
  • actionRequired (optional): Why your action is needed, when the status is ACTION_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 platform
  • orderId: Unique identifier of the order
  • assignmentId: Unique identifier of the assignment
  • assignmentType (optional): Assignment type, when available
  • scheduledAt: New confirmed shooting start. Always present; null means 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 platform
  • orderId: Unique identifier of the order
  • assignmentId: Unique identifier of the assignment
  • assignmentType (optional): Assignment type, when available
  • estimatedDeliveryAt: 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 triggered
  • propertyId: Real estate property id
  • targetPlatform: 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-materials

Order & 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 --> [*]
StatusMeaning
PLACEDOrder received; no assignment has started yet.
IN_PROGRESSAt least one assignment has started (moved past CREATED).
COMPLETEDAll work finished and the order is finalized. Terminal.
CANCELLEDOrder 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
StatusApplies toMeaning
CREATEDallAssignment created; work not yet started.
PHOTOGRAPHER_ASSIGNEDphoto/visualA creative is being scheduled (date/time pending).
SCHEDULEDphoto/visualShoot date/time confirmed with the creative.
IN_PRODUCTIONphoto/visualShoot done; visuals being edited.
RESCHEDULE_IN_PROCESSphoto/visualA reschedule is underway; a new creative or date is being arranged.
ACTION_REQUIREDdocumentWe need input from you (see actionRequired).
IN_PROCESSdocumentDocuments are being processed.
DELIVEREDallDeliverables sent to the client. Terminal.
CANCELLEDallAssignment 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/json
  • X-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:

  1. Respond quickly - Return a response within 30 seconds
  2. Return success status - HTTP 200 or 201 indicates successful processing
  3. Handle retries - Be idempotent as webhooks may be retried
  4. 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 lastTriggeredAt timestamp 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 materials

Testing

You can test your webhook integration using services like:

Security Considerations

  1. HTTPS Only - Always use HTTPS URLs for production webhooks
  2. Authentication - Use strong secrets for webhook authentication
  3. Secret Management - Secrets are never returned in API responses for security
  4. Validation - Validate webhook payloads in your endpoint
  5. Rate Limiting - Implement rate limiting on your webhook endpoints
  6. 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

  1. Webhook not triggering

    • Check if the webhook is enabled
    • Verify the URL is accessible
    • Check the authentication configuration
  2. Authentication failures

    • Verify the authentication type and secret
    • Check header/parameter names for custom auth
  3. 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/webhooks

Look for error logs in the service logs for debugging webhook call failures.


Did this page help you?