diff --git a/lib/wp/endpoint.c b/lib/wp/endpoint.c
index 3d6d0a93ead7305a96f06a9a83ba361c13a38902..df00742e1e49d35a9b5cac298e43d9383985a89c 100644
--- a/lib/wp/endpoint.c
+++ b/lib/wp/endpoint.c
@@ -682,6 +682,19 @@ on_stream_flags_changed (WpSessionItem * stream, WpSiFlags flags,
 {
   /* stream was deactivated; destroy the associated link */
   if (!(flags & WP_SI_FLAG_ACTIVE)) {
+    wp_trace_object (link, "destroying because stream " WP_OBJECT_FORMAT
+        " was deactivated", WP_OBJECT_ARGS (stream));
+    wp_session_item_reset (link);
+    g_object_unref (link);
+  }
+}
+
+static void
+on_link_flags_changed (WpSessionItem * link, WpSiFlags flags, gpointer data)
+{
+  const guint mask = (WP_SI_FLAG_EXPORTED | WP_SI_FLAG_EXPORT_ERROR);
+  if ((flags & mask) == mask) {
+    wp_trace_object (link, "destroying because impl proxy was destroyed");
     wp_session_item_reset (link);
     g_object_unref (link);
   }
@@ -855,6 +868,8 @@ impl_create_link (void *object, const struct spa_dict *props)
         G_CALLBACK (on_stream_flags_changed), link, 0);
     g_signal_connect_object (peer_si_stream, "flags-changed",
         G_CALLBACK (on_stream_flags_changed), link, 0);
+    g_signal_connect (link, "flags-changed",
+        G_CALLBACK (on_link_flags_changed), NULL);
 
     wp_session_item_export (link, session,
         (GAsyncReadyCallback) on_si_link_exported, self);
diff --git a/lib/wp/session-item.c b/lib/wp/session-item.c
index 906ba69e85dbb594baa08cf56a737336ccd118aa..a5b21800fd212960442b53f5b264f8086b359a8b 100644
--- a/lib/wp/session-item.c
+++ b/lib/wp/session-item.c
@@ -190,6 +190,7 @@ enum {
   EXPORT_STEP_ENDPOINT = WP_TRANSITION_STEP_CUSTOM_START,
   EXPORT_STEP_STREAMS,
   EXPORT_STEP_LINK,
+  EXPORT_STEP_CONNECT_DESTROYED,
 };
 
 static guint
@@ -229,6 +230,9 @@ wp_session_item_default_export_get_next_step (WpSessionItem * self,
 
   case EXPORT_STEP_LINK:
     g_return_val_if_fail (WP_IS_SI_LINK (self), WP_TRANSITION_STEP_ERROR);
+    return EXPORT_STEP_CONNECT_DESTROYED;
+
+  case EXPORT_STEP_CONNECT_DESTROYED:
     return WP_TRANSITION_STEP_NONE;
 
   default:
@@ -261,6 +265,36 @@ on_export_proxy_augmented (WpProxy * proxy, GAsyncResult * res, gpointer data)
   wp_transition_advance (transition);
 }
 
+static gboolean
+on_export_proxy_destroyed_deferred (WpSessionItem * self)
+{
+  WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);
+
+  g_return_val_if_fail (priv->impl_proxy, G_SOURCE_REMOVE);
+  g_return_val_if_fail (WP_SESSION_ITEM_GET_CLASS (self)->export_rollback,
+      G_SOURCE_REMOVE);
+
+  wp_info_object (self, "destroying " WP_OBJECT_FORMAT
+      " upon request by the server", WP_OBJECT_ARGS (priv->impl_proxy));
+
+  WP_SESSION_ITEM_GET_CLASS (self)->export_rollback (self);
+
+  priv->flags |= WP_SI_FLAG_EXPORT_ERROR;
+  g_signal_emit (self, signals[SIGNAL_FLAGS_CHANGED], 0, priv->flags);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+on_export_proxy_destroyed (WpProxy * proxy, gpointer data)
+{
+  WpSessionItem *self = WP_SESSION_ITEM (data);
+  g_autoptr (WpCore) core = wp_proxy_get_core (proxy);
+
+  wp_core_idle_add_closure (core, NULL, g_cclosure_new_object (
+          G_CALLBACK (on_export_proxy_destroyed_deferred), G_OBJECT (self)));
+}
+
 static void
 wp_session_item_default_export_execute_step (WpSessionItem * self,
     WpTransition * transition, guint step)
@@ -311,6 +345,12 @@ wp_session_item_default_export_execute_step (WpSessionItem * self,
         transition);
     break;
 
+  case EXPORT_STEP_CONNECT_DESTROYED:
+    g_signal_connect_object (priv->impl_proxy, "pw-proxy-destroyed",
+        G_CALLBACK (on_export_proxy_destroyed), self, 0);
+    wp_transition_advance (transition);
+    break;
+
   default:
     g_return_if_reached ();
   }
@@ -320,6 +360,8 @@ static void
 wp_session_item_default_export_rollback (WpSessionItem * self)
 {
   WpSessionItemPrivate *priv = wp_session_item_get_instance_private (self);
+  if (priv->impl_proxy)
+    g_signal_handlers_disconnect_by_data (priv->impl_proxy, self);
   g_clear_pointer (&priv->impl_streams, g_hash_table_unref);
   g_clear_object (&priv->impl_proxy);
   g_weak_ref_set (&priv->session, NULL);
diff --git a/tests/modules/si-standard-link.c b/tests/modules/si-standard-link.c
index 5933f26f09f1015f7e7defcb55b3aa34937bea11..08903bd5c872a271e97ea44e8e23e5a8bf94d0eb 100644
--- a/tests/modules/si-standard-link.c
+++ b/tests/modules/si-standard-link.c
@@ -235,6 +235,7 @@ test_si_standard_link_main (TestFixture * f, gconstpointer user_data)
     g_assert_cmpuint (wp_endpoint_link_get_state (ep_link, &error), ==,
         WP_ENDPOINT_LINK_STATE_ACTIVE);
     g_assert_null (error);
+    g_assert_cmpint (f->activation_state, ==, 2);
   }
 
   /* verify the graph state */
@@ -290,6 +291,7 @@ test_si_standard_link_main (TestFixture * f, gconstpointer user_data)
     g_assert_cmpuint (wp_endpoint_link_get_state (ep_link, &error), ==,
         WP_ENDPOINT_LINK_STATE_INACTIVE);
     g_assert_null (error);
+    g_assert_cmpint (f->activation_state, ==, 3);
   }
 
   /* verify the graph state */
@@ -328,6 +330,110 @@ test_si_standard_link_main (TestFixture * f, gconstpointer user_data)
   }
 }
 
+static void
+on_link_destroyed (WpEndpointLink * link, TestFixture * f)
+{
+  f->activation_state = 10;
+}
+
+static void
+test_si_standard_link_destroy (TestFixture * f, gconstpointer user_data)
+{
+  g_autoptr (WpSession) session_proxy = NULL;
+  g_autoptr (WpEndpoint) src_ep = NULL;
+  g_autoptr (WpEndpoint) sink_ep = NULL;
+  g_autoptr (WpEndpointLink) ep_link = NULL;
+
+  /* find the "audio" session from the client */
+  {
+    g_autoptr (WpObjectManager) om = wp_object_manager_new ();
+    wp_object_manager_add_interest_1 (om, WP_TYPE_SESSION, NULL);
+    wp_object_manager_request_proxy_features (om, WP_TYPE_SESSION,
+        WP_SESSION_FEATURES_STANDARD);
+    g_signal_connect_swapped (om, "installed",
+        G_CALLBACK (g_main_loop_quit), f->base.loop);
+    wp_core_install_object_manager (f->base.client_core, om);
+    g_main_loop_run (f->base.loop);
+
+    g_assert_nonnull (session_proxy =
+        wp_object_manager_lookup (om, WP_TYPE_SESSION,
+            WP_CONSTRAINT_TYPE_PW_PROPERTY, "session.name", "=s", "audio", NULL));
+    g_assert_cmpint (wp_proxy_get_bound_id (WP_PROXY (session_proxy)), ==,
+        wp_proxy_get_bound_id (WP_PROXY (f->session)));
+  }
+
+  /* find the endpoints */
+
+  g_assert_nonnull (src_ep =  wp_session_lookup_endpoint (session_proxy,
+          WP_CONSTRAINT_TYPE_PW_PROPERTY, "endpoint.name", "=s", "audiotestsrc",
+          NULL));
+  g_assert_nonnull (sink_ep =  wp_session_lookup_endpoint (session_proxy,
+          WP_CONSTRAINT_TYPE_PW_PROPERTY, "endpoint.name", "=s", "fakesink",
+          NULL));
+  g_assert_cmpuint (wp_endpoint_get_n_streams (src_ep), ==, 1);
+  g_assert_cmpuint (wp_endpoint_get_n_streams (sink_ep), ==, 1);
+
+  /* create the link */
+  {
+    g_autoptr (WpProperties) props = NULL;
+    g_autofree gchar * id =
+        g_strdup_printf ("%u", wp_proxy_get_bound_id (WP_PROXY (sink_ep)));
+
+    /* only the peer endpoint id is required,
+       everything else will be discovered */
+    props = wp_properties_new ("endpoint-link.input.endpoint", id, NULL);
+    wp_endpoint_create_link (src_ep, props);
+  }
+
+  g_signal_connect_swapped (session_proxy, "links-changed",
+      G_CALLBACK (g_main_loop_quit), f->base.loop);
+  g_main_loop_run (f->base.loop);
+
+  /* verify */
+
+  g_assert_cmpuint (wp_session_get_n_links (session_proxy), ==, 1);
+  g_assert_nonnull (ep_link = wp_session_lookup_link (session_proxy, NULL));
+  g_assert_cmpuint (wp_endpoint_link_get_state (ep_link, NULL), ==,
+      WP_ENDPOINT_LINK_STATE_INACTIVE);
+
+  /* activate */
+
+  g_signal_connect (ep_link, "state-changed",
+      G_CALLBACK (on_link_state_changed), f);
+  wp_endpoint_link_request_state (ep_link, WP_ENDPOINT_LINK_STATE_ACTIVE);
+  g_main_loop_run (f->base.loop);
+  g_assert_cmpuint (wp_endpoint_link_get_state (ep_link, NULL), ==,
+      WP_ENDPOINT_LINK_STATE_ACTIVE);
+
+  /* destroy */
+
+  g_signal_connect (ep_link, "pw-proxy-destroyed",
+      G_CALLBACK (on_link_destroyed), f);
+  wp_proxy_request_destroy (WP_PROXY (ep_link));
+
+  /* loop will quit because the "links-changed" signal from the session
+     is still connected to quit() from earlier */
+  g_main_loop_run (f->base.loop);
+
+  g_assert_cmpint (f->activation_state, ==, 10);
+  g_assert_cmpuint (wp_session_get_n_links (session_proxy), ==, 0);
+  g_assert_cmpuint (wp_proxy_get_bound_id (WP_PROXY (ep_link)), ==, (guint) -1);
+
+  /* verify the link was also destroyed on the session manager core */
+  {
+    g_autoptr (WpObjectManager) om = wp_object_manager_new ();
+
+    wp_object_manager_add_interest_1 (om, WP_TYPE_ENDPOINT_LINK, NULL);
+    g_signal_connect_swapped (om, "installed",
+        G_CALLBACK (g_main_loop_quit), f->base.loop);
+    wp_core_install_object_manager (f->base.core, om);
+    if (!wp_object_manager_is_installed (om))
+      g_main_loop_run (f->base.loop);
+
+    g_assert_cmpuint (wp_object_manager_get_n_objects (om), ==, 0);
+  }
+}
+
 gint
 main (gint argc, gchar *argv[])
 {
@@ -341,5 +447,11 @@ main (gint argc, gchar *argv[])
       test_si_standard_link_main,
       test_si_standard_link_teardown);
 
+  g_test_add ("/modules/si-standard-link/destroy",
+      TestFixture, NULL,
+      test_si_standard_link_setup,
+      test_si_standard_link_destroy,
+      test_si_standard_link_teardown);
+
   return g_test_run ();
 }