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/contact/ContactManagerImpl.java b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java
index f3138fd75fc6c6065f29d23430e93a7dbd4312eb..a03206ad2619faf5a22e6574a670472e015dd2a1 100644
--- a/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java
+++ b/bramble-core/src/main/java/org/briarproject/bramble/contact/ContactManagerImpl.java
@@ -147,7 +147,6 @@ class ContactManagerImpl implements ContactManager, EventListener {
 		} finally {
 			db.endTransaction(txn);
 		}
-		states.put(p.getId(), WAITING_FOR_CONNECTION);
 		return p;
 	}
 
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..d16317bffbe6b17ac74a26143f2f487939127b50 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.numEndpoints++;
+				}
 			}
+			if (cs.numEndpoints == 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.numEndpoints == 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.numEndpoints == 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 numEndpoints = 0;
+
 		private CryptoState(SecretKey rendezvousKey, boolean alice,
 				long expiry) {
 			this.rendezvousKey = rendezvousKey;
diff --git a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTest.java b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTest.java
index 0409571f9f38d22827731edc8ec2988ec90abd88..4bf7ae48d3acbaae3b3d711c60a4b9128839a95d 100644
--- a/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTest.java
+++ b/bramble-core/src/test/java/org/briarproject/bramble/contact/ContactExchangeIntegrationTest.java
@@ -6,11 +6,15 @@ import org.briarproject.bramble.api.contact.ContactManager;
 import org.briarproject.bramble.api.contact.PendingContact;
 import org.briarproject.bramble.api.contact.PendingContactState;
 import org.briarproject.bramble.api.contact.event.ContactAddedEvent;
+import org.briarproject.bramble.api.contact.event.PendingContactStateChangedEvent;
 import org.briarproject.bramble.api.crypto.PublicKey;
 import org.briarproject.bramble.api.crypto.SecretKey;
+import org.briarproject.bramble.api.event.Event;
+import org.briarproject.bramble.api.event.EventListener;
 import org.briarproject.bramble.api.identity.Identity;
 import org.briarproject.bramble.api.identity.IdentityManager;
 import org.briarproject.bramble.api.lifecycle.LifecycleManager;
+import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
 import org.briarproject.bramble.test.BrambleTestCase;
 import org.briarproject.bramble.test.TestDatabaseConfigModule;
 import org.briarproject.bramble.test.TestDuplexTransportConnection;
@@ -27,7 +31,7 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static junit.framework.TestCase.assertNotNull;
 import static junit.framework.TestCase.assertNull;
 import static junit.framework.TestCase.fail;
-import static org.briarproject.bramble.api.contact.PendingContactState.WAITING_FOR_CONNECTION;
+import static org.briarproject.bramble.api.contact.PendingContactState.OFFLINE;
 import static org.briarproject.bramble.test.TestDuplexTransportConnection.createPair;
 import static org.briarproject.bramble.test.TestPluginConfigModule.DUPLEX_TRANSPORT_ID;
 import static org.briarproject.bramble.test.TestUtils.deleteTestDirectory;
@@ -188,9 +192,14 @@ public class ContactExchangeIntegrationTest extends BrambleTestCase {
 	private PendingContact addPendingContact(
 			ContactExchangeIntegrationTestComponent local,
 			ContactExchangeIntegrationTestComponent remote) throws Exception {
+		EventWaiter waiter = new EventWaiter();
+		local.getEventBus().addListener(waiter);
 		String link = remote.getContactManager().getHandshakeLink();
 		String alias = remote.getIdentityManager().getLocalAuthor().getName();
-		return local.getContactManager().addPendingContact(link, alias);
+		PendingContact pendingContact =
+				local.getContactManager().addPendingContact(link, alias);
+		waiter.latch.await(TIMEOUT, MILLISECONDS);
+		return pendingContact;
 	}
 
 	private void assertContacts(boolean verified,
@@ -237,7 +246,7 @@ public class ContactExchangeIntegrationTest extends BrambleTestCase {
 		assertEquals(1, pairs.size());
 		Pair<PendingContact, PendingContactState> pair =
 				pairs.iterator().next();
-		assertEquals(WAITING_FOR_CONNECTION, pair.getSecond());
+		assertEquals(OFFLINE, pair.getSecond());
 		PendingContact pendingContact = pair.getFirst();
 		assertEquals(expectedIdentity.getLocalAuthor().getName(),
 				pendingContact.getAlias());
@@ -261,4 +270,19 @@ public class ContactExchangeIntegrationTest extends BrambleTestCase {
 		tearDown(bob);
 		deleteTestDirectory(testDir);
 	}
+
+	@NotNullByDefault
+	private static class EventWaiter implements EventListener {
+
+		private final CountDownLatch latch = new CountDownLatch(1);
+
+		@Override
+		public void eventOccurred(Event e) {
+			if (e instanceof PendingContactStateChangedEvent) {
+				PendingContactStateChangedEvent p =
+						(PendingContactStateChangedEvent) e;
+				if (p.getPendingContactState() == OFFLINE) latch.countDown();
+			}
+		}
+	}
 }
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..f1abd03f20535df3aa1fa45ca31180e7ffe3abbe 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("");
+				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"