Handoff
Webhook handoff
Send handoffs to your own system through the handoff.requested webhook.
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
- Create a webhook endpoint under Developers → Webhooks and subscribe it to
handoff.requested(andhandoff.updatedif you want state changes). - Under Handoff → Routing, add Webhook as a target.
Sent with ShopAssist-Event-Type: handoff.requested. Acknowledge with any 2xx within 10 seconds.
| Field | Type | Description |
|---|---|---|
| api_versionrequired | "v1" | default "v1" |
| created_atrequired | string | format date-time |
| datarequired | HandoffRequestedData | `{ handoff; conversation; transcript (≤ 50); dashboard_url }` (`handoff.shopper` carries `identity_level` and `email_verified`). |
| data.conversationrequired | ApiConversation | `ConversationSummary` without `last_message_preview`, plus `platform_customer_id`. |
| data.conversation.assigned_user_idrequired | string | null | pattern `^usr_[0-9a-hjkmnp-tv-z]{26}$` |
| data.conversation.channelrequired | "widget" | "playground" | "preview" | "demo" | "api" | |
| data.conversation.closed_atrequired | string | null | format date-time |
| data.conversation.csat_scorerequired | integer | null | |
| data.conversation.handoff_requested_atrequired | string | null | format date-time |
| data.conversation.handoff_staterequired | "none" | "requested" | "live" | "delivered_offline" | "failed" | "resolved" | |
| data.conversation.idrequired | string | pattern `^conv_[0-9a-hjkmnp-tv-z]{26}$` |
| data.conversation.identity_levelrequired | "guest" | "order_lookup" | "order_otp" | "customer" | |
| data.conversation.is_testrequired | boolean | |
| data.conversation.last_message_atrequired | string | null | format date-time |
| data.conversation.localerequired | string | null | |
| data.conversation.message_countrequired | integer | ≥ 0 |
| data.conversation.moderequired | "ai" | "human_requested" | "human" | |
| data.conversation.platform_customer_idrequired | string | null | Only for identity_level = customer |
| data.conversation.resolutionrequired | "resolved_ai" | "handed_off" | "abandoned" | "unknown" | null | |
| data.conversation.shopperrequired | ShopperView | Shopper identity as shown on lists (masked for agent/viewer, 06 §5.3). |
| data.conversation.shopper.emailrequired | string | null | |
| data.conversation.shopper.is_maskedrequired | boolean | |
| data.conversation.shopper.is_verifiedrequired | boolean | |
| data.conversation.shopper.namerequired | string | null | |
| data.conversation.started_atrequired | string | format date-time |
| data.conversation.statusrequired | "open" | "closed" | |
| data.conversation.store_idrequired | string | pattern `^sto_[0-9a-hjkmnp-tv-z]{26}$` |
| data.conversation.tagsrequired | string[] | |
| data.dashboard_urlrequired | string | format uri · ≤ 2048 characters |
| data.handoffrequired | HandoffAdmin | `HandoffAdmin = HandoffState & {…}` (03 §6). |
| data.handoff.accepted_atrequired | string | null | format date-time |
| data.handoff.agentrequired | AgentRef | null | |
| data.handoff.agent.display_namerequired | string | |
| data.handoff.delivered_atrequired | string | null | format date-time |
| data.handoff.expected_wait_secondsrequired | integer | null | |
| data.handoff.idrequired | string | pattern `^hof_[0-9a-hjkmnp-tv-z]{26}$` |
| data.handoff.messagerequired | string | Localized shopper-facing status text |
| data.handoff.reasonrequired | string (enum) | one of shopper_request, ai_escalation, action_request, policy, repeated_failure, error, outside_hours, quota_block, limit_reached |
| data.handoff.requested_atrequired | string | format date-time |
| data.handoff.requested_byrequired | "shopper" | "ai" | "agent" | "system" | |
| data.handoff.shopperrequired | HandoffShopper | `HandoffAdmin.shopper` (`platform_customer_id` only for `identity_level = customer`). |
| data.handoff.shopper.emailrequired | string | null | |
| data.handoff.shopper.email_verifiedrequired | boolean | |
| data.handoff.shopper.identity_levelrequired | "guest" | "order_lookup" | "order_otp" | "customer" | |
| data.handoff.shopper.namerequired | string | null | |
| data.handoff.shopper.platform_customer_idrequired | string | null | |
| data.handoff.staterequired | "requested" | "waiting_for_agent" | "live" | "delivered_offline" | "failed" | "resolved" | |
| data.handoff.statusrequired | string (enum) | one of requested, waiting_for_agent, live, delivering, delivered, failed, resolved, cancelled |
| data.handoff.summaryrequired | string | null | |
| data.handoff.targetrequired | "live_chat" | "email" | "helpdesk" | null | |
| data.handoff.targetsrequired | HandoffTargetView[] | |
| data.handoff.targets[].external_urlrequired | string | null | format uri · ≤ 2048 characters |
| data.handoff.targets[].integration_idrequired | string | null | pattern `^int_[0-9a-hjkmnp-tv-z]{26}$` |
| data.handoff.targets[].last_errorrequired | string | null | |
| data.handoff.targets[].statusrequired | "pending" | "delivered" | "failed" | "skipped" | |
| data.handoff.targets[].typerequired | string | |
| data.transcriptrequired | ApiMessage[] | ≤ 50 items |
| data.transcript[].authorrequired | MessageAuthor | Who wrote a message. |
| data.transcript[].author.display_namerequired | string | null | |
| data.transcript[].author.typerequired | "shopper" | "ai" | "human" | "system" | |
| data.transcript[].blocksrequired | UiBlock[] | |
| data.transcript[].created_atrequired | string | format date-time |
| data.transcript[].idrequired | string | pattern `^msg_[0-9a-hjkmnp-tv-z]{26}$` |
| data.transcript[].is_ai_generatedrequired | boolean | |
| data.transcript[].rolerequired | "shopper" | "assistant" | "agent" | "system" | |
| data.transcript[].seqrequired | integer | ≥ 0 |
| data.transcript[].textrequired | string | |
| idrequired | string | pattern `^evt_[0-9a-hjkmnp-tv-z]{26}$` |
| organization_idrequired | string | pattern `^org_[0-9a-hjkmnp-tv-z]{26}$` |
| store_idrequired | string | null | null for organization-level events (usage)pattern `^sto_[0-9a-hjkmnp-tv-z]{26}$` |
| typerequired | "handoff.requested" |
{
"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. Because webhooks are notifications only, also keep a shopper-facing target (email or a helpdesk) in your list.
Last updated on