# Webhook handoff

> Send handoffs to your own system through the handoff.requested webhook.

Source: https://shopassist.coec.dk/docs/handoff/webhook · Updated 2026-10-01

*(Growth and up)*

The webhook target is a notification target: it lets your own systems react to handoffs, for example to open a ticket in a tool ShopAssist does not integrate with.

## Set up [#setup]

1. Create a webhook endpoint under **Developers → Webhooks** and subscribe it to `handoff.requested` (and `handoff.updated` if you want state changes).
2. Under **Handoff → Routing**, add **Webhook** as a target.

Header `ShopAssist-Event-Type: handoff.requested`. Acknowledge with any 2xx within 10 seconds.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `api_version` | `"v1"` | yes | default "v1" |
| `created_at` | `string` | yes | format date-time |
| `data` | `HandoffRequestedData` | yes | `{ handoff; conversation; transcript (≤ 50); dashboard_url }` (`handoff.shopper` carries `identity_level` and `email_verified`). |
| `data.conversation` | `ApiConversation` | yes | `ConversationSummary` without `last_message_preview`, plus `platform_customer_id`. |
| `data.conversation.assigned_user_id` | `string \| null` | yes | pattern `^usr_[0-9a-hjkmnp-tv-z]{26}$` |
| `data.conversation.channel` | `"widget" \| "playground" \| "preview" \| "demo" \| "api"` | yes |  |
| `data.conversation.closed_at` | `string \| null` | yes | format date-time |
| `data.conversation.csat_score` | `integer \| null` | yes |  |
| `data.conversation.handoff_requested_at` | `string \| null` | yes | format date-time |
| `data.conversation.handoff_state` | `"none" \| "requested" \| "live" \| "delivered_offline" \| "failed" \| "resolved"` | yes |  |
| `data.conversation.id` | `string` | yes | pattern `^conv_[0-9a-hjkmnp-tv-z]{26}$` |
| `data.conversation.identity_level` | `"guest" \| "order_lookup" \| "order_otp" \| "customer"` | yes |  |
| `data.conversation.is_test` | `boolean` | yes |  |
| `data.conversation.last_message_at` | `string \| null` | yes | format date-time |
| `data.conversation.locale` | `string \| null` | yes |  |
| `data.conversation.message_count` | `integer` | yes | ≥ 0 |
| `data.conversation.mode` | `"ai" \| "human_requested" \| "human"` | yes |  |
| `data.conversation.platform_customer_id` | `string \| null` | yes | Only for identity_level = customer |
| `data.conversation.resolution` | `"resolved_ai" \| "handed_off" \| "abandoned" \| "unknown" \| null` | yes |  |
| `data.conversation.shopper` | `ShopperView` | yes | Shopper identity as shown on lists (masked for agent/viewer, 06 §5.3). |
| `data.conversation.shopper.email` | `string \| null` | yes |  |
| `data.conversation.shopper.is_masked` | `boolean` | yes |  |
| `data.conversation.shopper.is_verified` | `boolean` | yes |  |
| `data.conversation.shopper.name` | `string \| null` | yes |  |
| `data.conversation.started_at` | `string` | yes | format date-time |
| `data.conversation.status` | `"open" \| "closed"` | yes |  |
| `data.conversation.store_id` | `string` | yes | pattern `^sto_[0-9a-hjkmnp-tv-z]{26}$` |
| `data.conversation.tags` | `string[]` | yes |  |
| `data.dashboard_url` | `string` | yes | format uri · ≤ 2048 characters |
| `data.handoff` | `HandoffAdmin` | yes | `HandoffAdmin = HandoffState & {…}` (03 §6). |
| `data.handoff.accepted_at` | `string \| null` | yes | format date-time |
| `data.handoff.agent` | `AgentRef \| null` | yes |  |
| `data.handoff.agent.display_name` | `string` | yes |  |
| `data.handoff.delivered_at` | `string \| null` | yes | format date-time |
| `data.handoff.expected_wait_seconds` | `integer \| null` | yes |  |
| `data.handoff.id` | `string` | yes | pattern `^hof_[0-9a-hjkmnp-tv-z]{26}$` |
| `data.handoff.message` | `string` | yes | Localized shopper-facing status text |
| `data.handoff.reason` | `string (enum)` | yes | one of shopper_request, ai_escalation, action_request, policy, repeated_failure, error, outside_hours, quota_block, limit_reached |
| `data.handoff.requested_at` | `string` | yes | format date-time |
| `data.handoff.requested_by` | `"shopper" \| "ai" \| "agent" \| "system"` | yes |  |
| `data.handoff.shopper` | `HandoffShopper` | yes | `HandoffAdmin.shopper` (`platform_customer_id` only for `identity_level = customer`). |
| `data.handoff.shopper.email` | `string \| null` | yes |  |
| `data.handoff.shopper.email_verified` | `boolean` | yes |  |
| `data.handoff.shopper.identity_level` | `"guest" \| "order_lookup" \| "order_otp" \| "customer"` | yes |  |
| `data.handoff.shopper.name` | `string \| null` | yes |  |
| `data.handoff.shopper.platform_customer_id` | `string \| null` | yes |  |
| `data.handoff.state` | `"requested" \| "waiting_for_agent" \| "live" \| "delivered_offline" \| "failed" \| "resolved"` | yes |  |
| `data.handoff.status` | `string (enum)` | yes | one of requested, waiting_for_agent, live, delivering, delivered, failed, resolved, cancelled |
| `data.handoff.summary` | `string \| null` | yes |  |
| `data.handoff.target` | `"live_chat" \| "email" \| "helpdesk" \| null` | yes |  |
| `data.handoff.targets` | `HandoffTargetView[]` | yes |  |
| `data.handoff.targets[].external_url` | `string \| null` | yes | format uri · ≤ 2048 characters |
| `data.handoff.targets[].integration_id` | `string \| null` | yes | pattern `^int_[0-9a-hjkmnp-tv-z]{26}$` |
| `data.handoff.targets[].last_error` | `string \| null` | yes |  |
| `data.handoff.targets[].status` | `"pending" \| "delivered" \| "failed" \| "skipped"` | yes |  |
| `data.handoff.targets[].type` | `string` | yes |  |
| `data.transcript` | `ApiMessage[]` | yes | ≤ 50 items |
| `data.transcript[].author` | `MessageAuthor` | yes | Who wrote a message. |
| `data.transcript[].author.display_name` | `string \| null` | yes |  |
| `data.transcript[].author.type` | `"shopper" \| "ai" \| "human" \| "system"` | yes |  |
| `data.transcript[].blocks` | `UiBlock[]` | yes |  |
| `data.transcript[].created_at` | `string` | yes | format date-time |
| `data.transcript[].id` | `string` | yes | pattern `^msg_[0-9a-hjkmnp-tv-z]{26}$` |
| `data.transcript[].is_ai_generated` | `boolean` | yes |  |
| `data.transcript[].role` | `"shopper" \| "assistant" \| "agent" \| "system"` | yes |  |
| `data.transcript[].seq` | `integer` | yes | ≥ 0 |
| `data.transcript[].text` | `string` | yes |  |
| `id` | `string` | yes | pattern `^evt_[0-9a-hjkmnp-tv-z]{26}$` |
| `organization_id` | `string` | yes | pattern `^org_[0-9a-hjkmnp-tv-z]{26}$` |
| `store_id` | `string \| null` | yes | null for organization-level events (usage) · pattern `^sto_[0-9a-hjkmnp-tv-z]{26}$` |
| `type` | `"handoff.requested"` | yes |  |

```json
{
  "api_version": "v1",
  "created_at": "2026-09-28T10:15:30.123Z",
  "data": {
    "conversation": {
      "assigned_user_id": "usr_01j8000000e008000000000000",
      "channel": "widget",
      "closed_at": "2026-09-28T10:15:30.123Z",
      "csat_score": 1,
      "handoff_requested_at": "2026-09-28T10:15:30.123Z",
      "handoff_state": "none",
      "id": "conv_01j8000000e008000000000000",
      "identity_level": "guest",
      "is_test": true,
      "last_message_at": "2026-09-28T10:15:30.123Z",
      "locale": "da-DK",
      "message_count": 0,
      "mode": "ai",
      "platform_customer_id": "string",
      "resolution": "resolved_ai",
      "shopper": {
        "email": "string",
        "is_masked": true,
        "is_verified": true,
        "name": "string"
      },
      "started_at": "2026-09-28T10:15:30.123Z",
      "status": "open",
      "store_id": "sto_01j8000000e008000000000000",
      "tags": [
        "string"
      ]
    },
    "dashboard_url": "https://shop.example/products/linen-shirt",
    "handoff": {
      "agent": null,
      "expected_wait_seconds": 120,
      "id": "hof_01j8000000e008000000000001",
      "message": "We've let the team know. Someone will be with you shortly.",
      "state": "waiting_for_agent",
      "target": "live_chat"
    },
    "transcript": [
      {
        "author": {
          "display_name": "string",
          "type": "shopper"
        },
        "blocks": [
          {
            "as_of": "2026-09-28T10:15:30.123Z",
            "id": "b1",
            "items": [
              {
                "actions": {
                  "add_to_cart": true,
                  "check_store_stock": true,
                  "notify_back_in_stock": false
                },
                "as_of": "2026-09-28T10:15:30.123Z",
                "availability": {
                  "as_of": "2026-09-28T10:15:30.123Z",
                  "label": "Only 2 left",
                  "quantity": 2,
                  "status": "low_stock"
                },
                "badges": [
                  "New"
                ],
                "default_variant_id": "var_01j8000000e008000000000001",
                "image": {
                  "alt": "City bike",
                  "height": 600,
                  "url": "https://cdn.shopify.com/s/files/1/demo/bike-1.jpg",
                  "width": 800
                },
                "options": [
                  {
                    "name": "Size",
                    "values": [
                      {
                        "is_available": true,
                        "value": "M"
                      },
                      {
                        "is_available": false,
                        "value": "L"
                      }
                    ]
                  }
                ],
                "price": {
                  "amount": "4999",
                  "currency": "DKK",
                  "display": "4.999,00 kr."
                },
                "product_id": "prd_01j8000000e008000000000001",
                "rating": {
                  "average": 4.6,
                  "count": 128
                },
                "subtitle": "Nordic Bikes",
                "title": "Nordic City Bike 7-speed",
                "url": "https://nordicbikes.example/products/city-bike?utm_source=shopassist&utm_medium=chat",
                "variants": [
                  {
                    "availability": {
                      "as_of": "2026-09-28T10:15:30.123Z",
                      "label": "Only 2 left",
                      "quantity": 2,
                      "status": "low_stock"
                    },
                    "cart_ref": {
                      "variant_gid": "gid://shopify/ProductVariant/4711"
                    },
                    "options": {
                      "Size": "M"
                    },
                    "price": {
                      "amount": "4999",
                      "currency": "DKK",
                      "display": "4.999,00 kr."
                    },
                    "title": "M",
                    "variant_id": "var_01j8000000e008000000000001"
                  }
                ]
              }
            ],
            "layout": "carousel",
            "title": "City bikes in stock",
            "type": "products"
          }
        ],
        "created_at": "2026-09-28T10:15:30.123Z",
        "id": "msg_01j8000000e008000000000000",
        "is_ai_generated": true,
        "role": "shopper",
        "seq": 0,
        "text": "string"
      }
    ]
  },
  "id": "evt_01j8000000e008000000000000",
  "organization_id": "org_01j8000000e008000000000000",
  "store_id": "sto_01j8000000e008000000000000",
  "type": "handoff.requested"
}
```

Verify every delivery as described in [verifying signatures](/docs/webhooks/verifying-signatures). Because webhooks are notifications only, also keep a shopper-facing target (email or a helpdesk) in your list.
