diff --git a/pom.xml b/pom.xml
index f96a8fc7c287851c11d065e3072b12e62c1433ea..ec25d664a9d8331134c05faca6ce5b9f51ba10d6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -62,6 +62,18 @@
             <version>0.8.2</version>
         </dependency>
 
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+            <version>4.9.0</version>
+        </dependency>
+
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>logging-interceptor</artifactId>
+            <version>4.9.0</version>
+        </dependency>
+
         <dependency>
             <groupId>de.gultsch.ejabberd</groupId>
             <artifactId>ejabberd-api</artifactId>
diff --git a/src/main/java/de/gultsch/xmpp/addr/adapter/Adapter.java b/src/main/java/de/gultsch/xmpp/addr/adapter/Adapter.java
index 52a52eb600a9b330037a2f7a79b95911bc9b8c9f..ed6eb323a29a17d6722e2253f839ec9b6763d47c 100644
--- a/src/main/java/de/gultsch/xmpp/addr/adapter/Adapter.java
+++ b/src/main/java/de/gultsch/xmpp/addr/adapter/Adapter.java
@@ -3,7 +3,6 @@ package de.gultsch.xmpp.addr.adapter;
 import com.google.gson.GsonBuilder;
 import de.gultsch.xmpp.addr.adapter.gson.JidDeserializer;
 import de.gultsch.xmpp.addr.adapter.gson.JidSerializer;
-import de.gultsch.xmpp.addr.adapter.sql2o.IllegalJidStrategy;
 import de.gultsch.xmpp.addr.adapter.sql2o.JidConverter;
 import org.sql2o.converters.Converter;
 import rocks.xmpp.addr.Jid;
@@ -17,12 +16,9 @@ public class Adapter {
         gsonBuilder.registerTypeAdapter(Jid.class, new JidSerializer());
     }
 
-    public static void register(Map<Class, Converter> converters) {
-        register(converters, IllegalJidStrategy.THROW);
-    }
 
-    public static void register(Map<Class, Converter> converters, IllegalJidStrategy illegalJidStrategy) {
-        final JidConverter jidConverter = new JidConverter(illegalJidStrategy);
+    public static void register(Map<Class, Converter> converters) {
+        final JidConverter jidConverter = new JidConverter();
         converters.put(Jid.class, jidConverter);
         try {
             converters.put(Class.forName("rocks.xmpp.addr.FullJid"), jidConverter);
diff --git a/src/main/java/im/quicksy/server/configuration/Configuration.java b/src/main/java/im/quicksy/server/configuration/Configuration.java
index 0cab9aaa3ca1b126183ed5cb34e68cc19a1bad3b..d33bdf6338a1452726713bf6c510de50b8a40ede 100644
--- a/src/main/java/im/quicksy/server/configuration/Configuration.java
+++ b/src/main/java/im/quicksy/server/configuration/Configuration.java
@@ -44,6 +44,9 @@ public class Configuration {
     private HashMap<String, DatabaseConfiguration> db;
     private PayPal payPal = new PayPal();
     private String twilioAuthToken;
+
+    private String nexmoApiKey;
+    private String nexmoApiSecret;
     private String cimAuthToken;
     private Version minVersion;
     private Duration accountInactivity = Duration.ofDays(28);
@@ -134,6 +137,14 @@ public class Configuration {
         return twilioAuthToken;
     }
 
+    public String getNexmoApiKey() {
+        return nexmoApiKey;
+    }
+
+    public String getNexmoApiSecret() {
+        return nexmoApiSecret;
+    }
+
     public Optional<String> getCimAuthToken() {
         return Optional.ofNullable(cimAuthToken);
     }
diff --git a/src/main/java/im/quicksy/server/controller/BaseController.java b/src/main/java/im/quicksy/server/controller/BaseController.java
index 104bf4d547f481a61362a70f304dab99cfaa7394..d037d27dfdc0e70b273fcd31fe1b287e308eb4f6 100644
--- a/src/main/java/im/quicksy/server/controller/BaseController.java
+++ b/src/main/java/im/quicksy/server/controller/BaseController.java
@@ -21,6 +21,7 @@ import com.github.zafarkhaja.semver.Version;
 import com.google.common.base.Splitter;
 import com.google.common.net.InetAddresses;
 import im.quicksy.server.configuration.Configuration;
+import im.quicksy.server.verification.NexmoVerificationProvider;
 import im.quicksy.server.verification.TwilioVerificationProvider;
 import im.quicksy.server.verification.VerificationProvider;
 import org.slf4j.Logger;
@@ -51,7 +52,7 @@ public class BaseController {
     protected static Pattern PIN_PATTERN = Pattern.compile("^[0-9]{6}$");
     protected static Pattern UUID_PATTERN = Pattern.compile("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");
 
-    protected static final VerificationProvider VERIFICATION_PROVIDER = new TwilioVerificationProvider();
+    protected static final VerificationProvider VERIFICATION_PROVIDER = new NexmoVerificationProvider();
 
     protected static InetAddress getClientIp(Request request) {
         final InetAddress remote = InetAddresses.forString(request.ip());
diff --git a/src/main/java/im/quicksy/server/controller/EnterController.java b/src/main/java/im/quicksy/server/controller/EnterController.java
index 6fb3ad835383cdf04439c65ac8d3bf1daa9b5d1b..5a56f588f82fc9592f8bca0702734ef24cd032a9 100644
--- a/src/main/java/im/quicksy/server/controller/EnterController.java
+++ b/src/main/java/im/quicksy/server/controller/EnterController.java
@@ -31,6 +31,7 @@ import im.quicksy.server.pojo.Voucher;
 import im.quicksy.server.utils.CimUtils;
 import im.quicksy.server.utils.CodeGenerator;
 import im.quicksy.server.utils.PayPal;
+import im.quicksy.server.verification.NexmoVerificationProvider;
 import im.quicksy.server.verification.TwilioVerificationProvider;
 import im.quicksy.server.verification.VerificationProvider;
 import org.slf4j.Logger;
@@ -44,6 +45,8 @@ public class EnterController extends BaseController {
 
     private static final Logger LOGGER = LoggerFactory.getLogger(EnterController.class);
 
+    private static final VerificationProvider VERIFICATION_PROVIDER = new TwilioVerificationProvider();
+
     public static TemplateViewRoute intro = (request, response) -> {
         HashMap<String, Object> model = new HashMap<>();
         model.put("fee", Payment.FEE);
diff --git a/src/main/java/im/quicksy/server/controller/PasswordController.java b/src/main/java/im/quicksy/server/controller/PasswordController.java
index 2f6cb64c0ec32b19df303ec48757aa5948cd1ef0..99dee69c742ff7b05ff9d564ed548ca0da8baee0 100644
--- a/src/main/java/im/quicksy/server/controller/PasswordController.java
+++ b/src/main/java/im/quicksy/server/controller/PasswordController.java
@@ -28,6 +28,7 @@ import im.quicksy.server.ejabberd.MyEjabberdApi;
 import im.quicksy.server.throttle.RateLimiter;
 import im.quicksy.server.throttle.Strategy;
 import im.quicksy.server.verification.RequestFailedException;
+import im.quicksy.server.verification.TokenExpiredException;
 import im.quicksy.server.verification.TwilioVerificationProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -90,7 +91,7 @@ public class PasswordController extends BaseController {
                     if (MyEjabberdApi.getInstance().checkAccount(jid.getEscapedLocal(), jid.getDomain())) {
                         final Last last = MyEjabberdApi.getInstance().getLast(jid.getEscapedLocal(), jid.getDomain());
                         final Duration lastActivity = Duration.between(last.getTimestamp(), Instant.now());
-                        LOGGER.info("user "+jid.getEscapedLocal()+" was last active "+lastActivity+" ago.");
+                        LOGGER.info("user " + jid.getEscapedLocal() + " was last active " + lastActivity + " ago.");
                         if (Configuration.getInstance().getAccountInactivity().minus(lastActivity).isNegative()) {
                             LOGGER.info("delete old and create new user " + jid);
                             MyEjabberdApi.getInstance().unregister(jid.getEscapedLocal(), jid.getDomain());
@@ -110,13 +111,12 @@ public class PasswordController extends BaseController {
                     System.out.println("verification provider reported failed");
                     return halt(401);
                 }
+            } catch (TokenExpiredException e) {
+                LOGGER.warn("Contacting verification provider failed with: " + e.getMessage());
+                return halt(404);
             } catch (RequestFailedException e) {
-                if (e.getCode() == TwilioVerificationProvider.PHONE_VERIFICATION_NOT_FOUND) {
-                    return halt(404);
-                } else {
-                    LOGGER.warn("Contacting verification provider failed with: " + e.getMessage());
-                    return halt(500);
-                }
+                LOGGER.warn("Contacting verification provider failed with: " + e.getMessage());
+                return halt(500);
             } catch (de.gultsch.ejabberd.api.RequestFailedException e) {
                 LOGGER.warn("Contacting ejabberd failed with: " + e.getMessage());
                 return halt(500);
diff --git a/src/main/java/im/quicksy/server/verification/NexmoVerificationProvider.java b/src/main/java/im/quicksy/server/verification/NexmoVerificationProvider.java
new file mode 100644
index 0000000000000000000000000000000000000000..e8df3fe5f643f6db7eb4e42ddc835d938514bb99
--- /dev/null
+++ b/src/main/java/im/quicksy/server/verification/NexmoVerificationProvider.java
@@ -0,0 +1,143 @@
+package im.quicksy.server.verification;
+
+import com.google.common.base.Strings;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.math.IntMath;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.i18n.phonenumbers.Phonenumber;
+import im.quicksy.server.configuration.Configuration;
+import im.quicksy.server.verification.nexmo.GenericResponse;
+import okhttp3.*;
+import okhttp3.logging.HttpLoggingInterceptor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.security.SecureRandom;
+import java.time.Duration;
+import java.util.List;
+
+public class NexmoVerificationProvider implements VerificationProvider {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(NexmoVerificationProvider.class);
+
+    private static final OkHttpClient OK_HTTP_CLIENT = new OkHttpClient.Builder()
+            //.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY))
+            .build();
+
+    private static final Gson GSON = new GsonBuilder().create();
+
+    private static final HttpUrl NEXMO_API_URL = HttpUrl.get("https://rest.nexmo.com/sms/json");
+
+    private static final String BRAND_NAME = "Quicksy.im";
+    private static final String MESSAGE = "Your Quicksy code is: %s\n\nDon't share this code with others.\n\nOYITl6r6eIp";
+
+    private static final int MAX_ATTEMPTS = 3;
+    private static final SecureRandom SECURE_RANDOM = new SecureRandom();
+    private final Cache<Phonenumber.PhoneNumber, Pin> PIN_CACHE = CacheBuilder.newBuilder()
+            .expireAfterWrite(Duration.ofMinutes(5))
+            .build();
+
+    @Override
+    public boolean verify(Phonenumber.PhoneNumber phoneNumber, String input) throws RequestFailedException {
+        final Pin pin = PIN_CACHE.getIfPresent(phoneNumber);
+        if (pin == null) {
+            throw new TokenExpiredException("No pin found for this phone number");
+        }
+        try {
+            return pin.verify(input);
+        } catch (TooManyAttemptsException e) {
+            throw new TokenExpiredException(e);
+        }
+    }
+
+    @Override
+    public void request(Phonenumber.PhoneNumber phoneNumber, Method method) throws RequestFailedException {
+        final Pin pin = Pin.generate();
+        PIN_CACHE.put(phoneNumber, pin);
+        System.out.println("pin: " + pin);
+        final String to = String.format("%d%d", phoneNumber.getCountryCode(), phoneNumber.getNationalNumber());
+        LOGGER.info("requesting SMS through nexmo for {}", to);
+        final Call call = OK_HTTP_CLIENT.newCall(new Request.Builder()
+                .post(new FormBody.Builder()
+                        .add("from", BRAND_NAME)
+                        .add("text", String.format(MESSAGE, pin.toString()))
+                        .add("to", to)
+                        .add("api_key", Configuration.getInstance().getNexmoApiKey())
+                        .add("api_secret", Configuration.getInstance().getNexmoApiSecret())
+                        .build())
+                .url(NEXMO_API_URL)
+                .build());
+        try {
+            final Response response = call.execute();
+            final int code = response.code();
+            if (code != 200) {
+                LOGGER.warn("failed to request SMS verification. error code was {}", code);
+                throw new RequestFailedException("Response code was " + code);
+            } else {
+                final ResponseBody body = response.body();
+                if (body == null) {
+                    throw new RequestFailedException("Empty body");
+                }
+                final GenericResponse nexmoResponse = GSON.fromJson(body.charStream(), GenericResponse.class);
+                final List<GenericResponse.Message> messages = nexmoResponse.getMessages();
+                if (messages.size() >= 1) {
+                    final GenericResponse.Message message = messages.get(0);
+                    final String status = message.getStatus();
+                    if (!"0".equals(status)) {
+                        LOGGER.error("Unable to requests SMS. Status={} text={}",message.getStatus(), message.getErrorText());
+                        throw new RequestFailedException(message.getErrorText());
+                    }
+                } else {
+                    throw new RequestFailedException("Invalid number of result messages");
+                }
+            }
+            LOGGER.info("call was successful");
+        } catch (IOException e) {
+            LOGGER.warn("failed to request SMS verification", e);
+            throw new RequestFailedException(e);
+        }
+    }
+
+    @Override
+    public void request(Phonenumber.PhoneNumber phoneNumber, Method method, String language) throws RequestFailedException {
+        request(phoneNumber, method);
+    }
+
+    public static class Pin {
+        private final String pin;
+        private int attempts = 0;
+
+        Pin(String pin) {
+            this.pin = pin;
+        }
+
+        public static Pin generate() {
+            final int pin = SECURE_RANDOM.nextInt(IntMath.pow(10, VerificationProvider.VERIFICATION_CODE_LENGTH));
+            return new Pin(Strings.padStart(
+                    String.valueOf(pin),
+                    VerificationProvider.VERIFICATION_CODE_LENGTH,
+                    '0'
+            ));
+        }
+
+        public synchronized boolean verify(String pin) {
+            if (this.attempts >= MAX_ATTEMPTS) {
+                throw new TooManyAttemptsException();
+            }
+            this.attempts++;
+            return this.pin.equals(pin);
+        }
+
+        @Override
+        public String toString() {
+            return this.pin;
+        }
+    }
+
+    public static class TooManyAttemptsException extends RuntimeException {
+
+    }
+}
diff --git a/src/main/java/im/quicksy/server/verification/TokenExpiredException.java b/src/main/java/im/quicksy/server/verification/TokenExpiredException.java
new file mode 100644
index 0000000000000000000000000000000000000000..af29a5f8d5dbc5faa285b932bd1f5a47e67ff442
--- /dev/null
+++ b/src/main/java/im/quicksy/server/verification/TokenExpiredException.java
@@ -0,0 +1,15 @@
+package im.quicksy.server.verification;
+
+public class TokenExpiredException extends RequestFailedException {
+    public TokenExpiredException(String message, int code) {
+        super(message, code);
+    }
+
+    public TokenExpiredException(String message) {
+        super(message,0);
+    }
+
+    public TokenExpiredException(Exception e) {
+        super(e);
+    }
+}
diff --git a/src/main/java/im/quicksy/server/verification/TwilioVerificationProvider.java b/src/main/java/im/quicksy/server/verification/TwilioVerificationProvider.java
index e389c13cd99554df1a2ee5c72646f1d1c1fbf7c6..d8ad2f91244aad3fc0f71be066756ed089c1bbdf 100644
--- a/src/main/java/im/quicksy/server/verification/TwilioVerificationProvider.java
+++ b/src/main/java/im/quicksy/server/verification/TwilioVerificationProvider.java
@@ -141,8 +141,12 @@ public class TwilioVerificationProvider implements VerificationProvider {
                 return gson.fromJson(result, clazz);
             } else {
                 LOGGER.debug("json was " + result);
-                ErrorResponse error = gson.fromJson(result, ErrorResponse.class);
-                throw new RequestFailedException(error.getMessage(), error.getErrorCode());
+                final ErrorResponse error = gson.fromJson(result, ErrorResponse.class);
+                if (error.getErrorCode() == PHONE_VERIFICATION_NOT_FOUND) {
+                    throw new TokenExpiredException(error.getMessage(), error.getErrorCode());
+                } else {
+                    throw new RequestFailedException(error.getMessage(), error.getErrorCode());
+                }
             }
         } catch (JsonSyntaxException e) {
             final String firstLine = result == null ? "" : result.split("\n")[0];
diff --git a/src/main/java/im/quicksy/server/verification/VerificationProvider.java b/src/main/java/im/quicksy/server/verification/VerificationProvider.java
index 75cd8aeeffb0064d3a08724bd112d8554d5cc56b..1b06f772aee27a78e028b510ffdb16ab71be9741 100644
--- a/src/main/java/im/quicksy/server/verification/VerificationProvider.java
+++ b/src/main/java/im/quicksy/server/verification/VerificationProvider.java
@@ -20,6 +20,8 @@ import com.google.i18n.phonenumbers.Phonenumber;
 
 public interface VerificationProvider {
 
+    int VERIFICATION_CODE_LENGTH = 6;
+
     boolean verify(Phonenumber.PhoneNumber phoneNumber, String pin) throws RequestFailedException;
 
     void request(Phonenumber.PhoneNumber phoneNumber, Method method) throws RequestFailedException;
diff --git a/src/main/java/im/quicksy/server/verification/nexmo/ErrorResponse.java b/src/main/java/im/quicksy/server/verification/nexmo/ErrorResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..3a47be4fb910ad1d2604cd6c77f82dafac6c2ce5
--- /dev/null
+++ b/src/main/java/im/quicksy/server/verification/nexmo/ErrorResponse.java
@@ -0,0 +1,9 @@
+package im.quicksy.server.verification.nexmo;
+
+public class ErrorResponse {
+
+    private String type;
+    private String title;
+    private String detail;
+
+}
diff --git a/src/main/java/im/quicksy/server/verification/nexmo/GenericResponse.java b/src/main/java/im/quicksy/server/verification/nexmo/GenericResponse.java
new file mode 100644
index 0000000000000000000000000000000000000000..df0f235e90c585e96c80983b4dbec0ccd52b002a
--- /dev/null
+++ b/src/main/java/im/quicksy/server/verification/nexmo/GenericResponse.java
@@ -0,0 +1,40 @@
+package im.quicksy.server.verification.nexmo;
+
+import com.google.gson.annotations.SerializedName;
+
+import java.util.List;
+
+public class GenericResponse {
+
+    @SerializedName("message_count")
+    private int messageCount;
+
+    private List<Message> messages;
+
+    public int getMessageCount() {
+        return messageCount;
+    }
+
+    public List<Message> getMessages() {
+        return messages;
+    }
+
+    public static class Message {
+        private String to;
+        private String status;
+        @SerializedName("error-text")
+        private String errorText;
+
+        public String getTo() {
+            return to;
+        }
+
+        public String getStatus() {
+            return status;
+        }
+
+        public String getErrorText() {
+            return errorText;
+        }
+    }
+}
diff --git a/src/test/java/im/quicksy/server/NexmoVerificationProviderTest.java b/src/test/java/im/quicksy/server/NexmoVerificationProviderTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..d258f44c2a981181413068a6cc229857e6d18b63
--- /dev/null
+++ b/src/test/java/im/quicksy/server/NexmoVerificationProviderTest.java
@@ -0,0 +1,23 @@
+package im.quicksy.server;
+
+import im.quicksy.server.verification.NexmoVerificationProvider;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+public class NexmoVerificationProviderTest {
+
+    @Rule
+    public final ExpectedException expectedException = ExpectedException.none();
+
+    @Test
+    public void pinExpiry() {
+        NexmoVerificationProvider.Pin pin = NexmoVerificationProvider.Pin.generate();
+        System.out.println(pin);
+        pin.verify("000000");
+        pin.verify("000000");
+        pin.verify("000000");
+        expectedException.expect(NexmoVerificationProvider.TooManyAttemptsException.class);
+        pin.verify("000000");
+    }
+}