diff --git a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/PendingContactState.java b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/PendingContactState.java
index 9fdb1344289b80c79ee2b18ad51a40bc7106d6fb..60ed7731863de22d99759efa077a7fee60c4cecc 100644
--- a/bramble-api/src/main/java/org/briarproject/bramble/api/contact/PendingContactState.java
+++ b/bramble-api/src/main/java/org/briarproject/bramble/api/contact/PendingContactState.java
@@ -3,6 +3,7 @@ package org.briarproject.bramble.api.contact;
 public enum PendingContactState {
 
 	WAITING_FOR_CONNECTION,
+	OFFLINE,
 	CONNECTING,
 	ADDING_CONTACT,
 	FAILED
diff --git a/bramble-core/src/main/java/org/briarproject/bramble/rendezvous/RendezvousPollerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/rendezvous/RendezvousPollerImpl.java
index 6f5ec72b70fed1c1b1306dd3b51510b8e81ba8be..00c1d4982819a1236d5a74fa76b8938cfbdf7107 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/rendezvous/RendezvousPollerImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/rendezvous/RendezvousPollerImpl.java
@@ -66,6 +66,7 @@ import static java.util.logging.Level.WARNING;
 import static java.util.logging.Logger.getLogger;
 import static org.briarproject.bramble.api.contact.PendingContactState.ADDING_CONTACT;
 import static org.briarproject.bramble.api.contact.PendingContactState.FAILED;
+import static org.briarproject.bramble.api.contact.PendingContactState.OFFLINE;
 import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION;
 import static org.briarproject.bramble.api.nullsafety.NullSafety.requireNull;
 import static org.briarproject.bramble.rendezvous.RendezvousConstants.POLLING_INTERVAL_MS;
@@ -158,9 +159,7 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener {
 	private void addPendingContact(PendingContact p) {
 		long now = clock.currentTimeMillis();
 		long expiry = p.getTimestamp() + RENDEZVOUS_TIMEOUT_MS;
-		if (expiry > now) {
-			broadcastState(p.getId(), WAITING_FOR_CONNECTION);
-		} else {
+		if (expiry <= now) {
 			broadcastState(p.getId(), FAILED);
 			return;
 		}
@@ -180,9 +179,13 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener {
 			for (PluginState ps : pluginStates.values()) {
 				RendezvousEndpoint endpoint =
 						createEndpoint(ps.plugin, p.getId(), cs);
-				if (endpoint != null)
+				if (endpoint != null) {
 					requireNull(ps.endpoints.put(p.getId(), endpoint));
+					cs.endpoints++;
+				}
 			}
+			if (cs.endpoints == 0) broadcastState(p.getId(), OFFLINE);
+			else broadcastState(p.getId(), WAITING_FOR_CONNECTION);
 		} catch (DbException | GeneralSecurityException e) {
 			logException(LOG, WARNING, e);
 		}
@@ -328,9 +331,14 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener {
 		TransportId t = plugin.getId();
 		Map<PendingContactId, RendezvousEndpoint> endpoints = new HashMap<>();
 		for (Entry<PendingContactId, CryptoState> e : cryptoStates.entrySet()) {
-			RendezvousEndpoint endpoint =
-					createEndpoint(plugin, e.getKey(), e.getValue());
-			if (endpoint != null) endpoints.put(e.getKey(), endpoint);
+			PendingContactId p = e.getKey();
+			CryptoState cs = e.getValue();
+			RendezvousEndpoint endpoint = createEndpoint(plugin, p, cs);
+			if (endpoint != null) {
+				endpoints.put(p, endpoint);
+				if (++cs.endpoints == 1)
+					broadcastState(p, WAITING_FOR_CONNECTION);
+			}
 		}
 		requireNull(pluginStates.put(t, new PluginState(plugin, endpoints)));
 	}
@@ -344,8 +352,11 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener {
 	private void removeTransport(TransportId t) {
 		PluginState ps = pluginStates.remove(t);
 		if (ps != null) {
-			for (RendezvousEndpoint endpoint : ps.endpoints.values()) {
-				tryToClose(endpoint, LOG, INFO);
+			for (Entry<PendingContactId, RendezvousEndpoint> e :
+					ps.endpoints.entrySet()) {
+				tryToClose(e.getValue(), LOG, INFO);
+				CryptoState cs = cryptoStates.get(e.getKey());
+				if (--cs.endpoints == 0) broadcastState(e.getKey(), OFFLINE);
 			}
 		}
 	}
@@ -391,6 +402,8 @@ class RendezvousPollerImpl implements RendezvousPoller, Service, EventListener {
 		private final boolean alice;
 		private final long expiry;
 
+		private int endpoints = 0;
+
 		private CryptoState(SecretKey rendezvousKey, boolean alice,
 				long expiry) {
 			this.rendezvousKey = rendezvousKey;
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/rendezvous/RendezvousPollerImplTest.java b/bramble-core/src/test/java/org/briarproject/bramble/rendezvous/RendezvousPollerImplTest.java
index 27321d748e9f23470c0662c8ed8e8c213f37647d..0f65d9adf5ca8f139d5ca42ac03d0844df22f41a 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/rendezvous/RendezvousPollerImplTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/rendezvous/RendezvousPollerImplTest.java
@@ -45,6 +45,7 @@ import static java.util.Collections.singletonList;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static org.briarproject.bramble.api.contact.PendingContactState.ADDING_CONTACT;
 import static org.briarproject.bramble.api.contact.PendingContactState.FAILED;
+import static org.briarproject.bramble.api.contact.PendingContactState.OFFLINE;
 import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION;
 import static org.briarproject.bramble.rendezvous.RendezvousConstants.POLLING_INTERVAL_MS;
 import static org.briarproject.bramble.rendezvous.RendezvousConstants.RENDEZVOUS_TIMEOUT_MS;
@@ -120,7 +121,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
 			will(returnValue(beforeExpiry));
 			oneOf(eventBus).broadcast(with(new PredicateMatcher<>(
 					PendingContactStateChangedEvent.class, e ->
-					e.getPendingContactState() == WAITING_FOR_CONNECTION)));
+					e.getPendingContactState() == OFFLINE)));
 			// Capture the poll task
 			oneOf(scheduler).scheduleAtFixedRate(with(any(Runnable.class)),
 					with(POLLING_INTERVAL_MS), with(POLLING_INTERVAL_MS),
@@ -184,7 +185,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
 		context.assertIsSatisfied();
 
 		// Add the pending contact - endpoint should be created and polled
-		expectAddUnexpiredPendingContact(beforeExpiry);
+		expectAddPendingContact(beforeExpiry, WAITING_FOR_CONNECTION);
 		expectDeriveRendezvousKey();
 		expectCreateEndpoint();
 
@@ -205,9 +206,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
 		context.assertIsSatisfied();
 
 		// Remove the pending contact - endpoint should be closed
-		context.checking(new Expectations() {{
-			oneOf(rendezvousEndpoint).close();
-		}});
+		expectCloseEndpoint();
 
 		rendezvousPoller.eventOccurred(
 				new PendingContactRemovedEvent(pendingContact.getId()));
@@ -238,7 +237,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
 		context.assertIsSatisfied();
 
 		// Add the pending contact - endpoint should be created and polled
-		expectAddUnexpiredPendingContact(beforeExpiry);
+		expectAddPendingContact(beforeExpiry, WAITING_FOR_CONNECTION);
 		expectDeriveRendezvousKey();
 		expectCreateEndpoint();
 
@@ -260,10 +259,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
 
 		// Run the poll task - pending contact expires, endpoint is closed
 		expectPendingContactExpires(afterExpiry);
-
-		context.checking(new Expectations() {{
-			oneOf(rendezvousEndpoint).close();
-		}});
+		expectCloseEndpoint();
 
 		capturePollTask.get().run();
 		context.assertIsSatisfied();
@@ -289,7 +285,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
 		context.assertIsSatisfied();
 
 		// Add the pending contact - no endpoints should be created yet
-		expectAddUnexpiredPendingContact(beforeExpiry);
+		expectAddPendingContact(beforeExpiry, OFFLINE);
 		expectDeriveRendezvousKey();
 
 		rendezvousPoller.eventOccurred(
@@ -299,14 +295,14 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
 		// Enable the transport - endpoint should be created
 		expectGetPlugin();
 		expectCreateEndpoint();
+		expectStateChangedEvent(WAITING_FOR_CONNECTION);
 
 		rendezvousPoller.eventOccurred(new TransportEnabledEvent(transportId));
 		context.assertIsSatisfied();
 
 		// Disable the transport - endpoint should be closed
-		context.checking(new Expectations() {{
-			oneOf(rendezvousEndpoint).close();
-		}});
+		expectCloseEndpoint();
+		expectStateChangedEvent(OFFLINE);
 
 		rendezvousPoller.eventOccurred(new TransportDisabledEvent(transportId));
 		context.assertIsSatisfied();
@@ -482,13 +478,14 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
 		return capturePollTask;
 	}
 
-	private void expectAddUnexpiredPendingContact(long now) {
+	private void expectAddPendingContact(long now,
+			PendingContactState initialState) {
 		context.checking(new Expectations() {{
 			oneOf(clock).currentTimeMillis();
 			will(returnValue(now));
 			oneOf(eventBus).broadcast(with(new PredicateMatcher<>(
 					PendingContactStateChangedEvent.class, e ->
-					e.getPendingContactState() == WAITING_FOR_CONNECTION)));
+					e.getPendingContactState() == initialState)));
 		}});
 	}
 
@@ -546,7 +543,7 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
 			will(returnValue(now));
 			oneOf(eventBus).broadcast(with(new PredicateMatcher<>(
 					PendingContactStateChangedEvent.class, e ->
-					e.getPendingContactState() == WAITING_FOR_CONNECTION)));
+					e.getPendingContactState() == OFFLINE)));
 			// Capture the poll task
 			oneOf(scheduler).scheduleAtFixedRate(with(any(Runnable.class)),
 					with(POLLING_INTERVAL_MS), with(POLLING_INTERVAL_MS),
@@ -576,4 +573,10 @@ public class RendezvousPollerImplTest extends BrambleMockTestCase {
 					e.getPendingContactState() == state)));
 		}});
 	}
+
+	private void expectCloseEndpoint() throws Exception {
+		context.checking(new Expectations() {{
+			oneOf(rendezvousEndpoint).close();
+		}});
+	}
 }
diff --git a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactViewHolder.java b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactViewHolder.java
index 980ef3bec013dfbb340954b169f617ce89e9c4a4..b5606e1d56009455d6df52e0107dcc0ceb9ec4d7 100644
--- a/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactViewHolder.java
+++ b/briar-android/src/main/java/org/briarproject/briar/android/contact/add/remote/PendingContactViewHolder.java
@@ -52,6 +52,11 @@ class PendingContactViewHolder extends ViewHolder {
 						.getColor(status.getContext(), R.color.briar_yellow);
 				status.setText(R.string.waiting_for_contact_to_come_online);
 				break;
+			case OFFLINE:
+				color = ContextCompat
+						.getColor(status.getContext(), R.color.briar_yellow);
+				status.setText(R.string.offline_state);
+				break;
 			case CONNECTING:
 				status.setText(R.string.connecting);
 				break;
diff --git a/briar-headless/README.md b/briar-headless/README.md
index 19004e25accecfcb361579b1008b6060437ffff6..59bcc64c37630af42a4761fedc98a620acfbc56c 100644
--- a/briar-headless/README.md
+++ b/briar-headless/README.md
@@ -131,6 +131,7 @@ This will return a JSON array of pending contacts and their states:
 The state can be one of these values:
 
   * `waiting_for_connection`
+  * `offline`
   * `connecting`
   * `adding_contact`
   * `failed`
diff --git a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputPendingContact.kt b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputPendingContact.kt
index b6226b627511d9aee80825280df6642ecec767a3..429edb44f82c14d23802a10183089705ef3deba8 100644
--- a/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputPendingContact.kt
+++ b/briar-headless/src/main/java/org/briarproject/briar/headless/contact/OutputPendingContact.kt
@@ -16,6 +16,7 @@ internal fun PendingContact.output() = JsonDict(
 
 internal fun PendingContactState.output() = when(this) {
     WAITING_FOR_CONNECTION -> "waiting_for_connection"
+    OFFLINE -> "offline"
     CONNECTING -> "connecting"
     ADDING_CONTACT -> "adding_contact"
     FAILED -> "failed"