diff --git a/api/joinroom.go b/api/joinroom.go
index 2767b39eee4d51ad88f82501354b8cc2a04c2c33..5826d7844ae78f05eab07696df28ea2ff1cef7bb 100644
--- a/api/joinroom.go
+++ b/api/joinroom.go
@@ -5,7 +5,7 @@ import (
 	"net/http"
 )
 
-func JoinRoom(userData LoginResponse, roomId string) error {
+func JoinRoom(token Token, roomId string) error {
 	request, err := http.NewRequest(
 		http.MethodPost,
 		fmt.Sprintf("https://matrix-client.matrix.org/_matrix/client/v3/rooms/%s/join", roomId),
@@ -14,7 +14,7 @@ func JoinRoom(userData LoginResponse, roomId string) error {
 	if err != nil {
 		panic(err)
 	}
-	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", userData.AccessToken))
+	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
 	_, err = http.DefaultClient.Do(request)
 	return err
 }
diff --git a/api/markread.go b/api/markread.go
index 2a522b1983948bd27bd6a1f8f25f77f96a00d6e9..7bbdaa7edf3707b4a5d0d97b0f272b040da9ba42 100644
--- a/api/markread.go
+++ b/api/markread.go
@@ -15,7 +15,7 @@ type ReadReceiptRequest struct {
 	ReadPrivate string `json:"m.read.private"`
 }
 
-func SetReadReceipt(userData LoginResponse, roomId string, messageId string) error {
+func SetReadReceipt(token Token, roomId string, messageId string) error {
 	body, err := json.Marshal(ReadReceiptRequest{
 		FullyRead:   messageId,
 		Read:        messageId,
@@ -32,7 +32,7 @@ func SetReadReceipt(userData LoginResponse, roomId string, messageId string) err
 	if err != nil {
 		panic(err)
 	}
-	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", userData.AccessToken))
+	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
 	response, err := http.DefaultClient.Do(request)
 	if err != nil {
 		return err
diff --git a/api/refresh.go b/api/refresh.go
new file mode 100644
index 0000000000000000000000000000000000000000..1dcc5b72cf6ddd4e8581eb12b0f356615c03e7f5
--- /dev/null
+++ b/api/refresh.go
@@ -0,0 +1,37 @@
+package api
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+type RefreshRequest struct {
+	RefreshToken string `json:"refresh_token"`
+}
+
+type RefreshResponse struct {
+	AccessToken  string `json:"access_token"`
+	ExpiresInMs  int    `json:"expires_in_ms"`
+	RefreshToken string `json:"refresh_token"`
+}
+
+func Refresh(refreshToken string) (RefreshResponse, error) {
+	body, err := json.Marshal(RefreshRequest{
+		RefreshToken: refreshToken,
+	})
+	if err != nil {
+		return RefreshResponse{}, err
+	}
+	resp, err := http.Post("https://matrix-client.matrix.org/_matrix/client/v3/refresh", "application/json", bytes.NewReader(body))
+	if err != nil {
+		return RefreshResponse{}, err
+	}
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		return RefreshResponse{}, fmt.Errorf("request failed %d: %s", resp.StatusCode, resp.Status)
+	}
+	var responseBody RefreshResponse
+	err = json.NewDecoder(resp.Body).Decode(&responseBody)
+	return responseBody, err
+}
diff --git a/api/sendmessage.go b/api/sendmessage.go
index 761d73d3db1d2b51124841bbeb5b259f86191905..70e480d43bd9453021140cea57419ce5c071ed74 100644
--- a/api/sendmessage.go
+++ b/api/sendmessage.go
@@ -8,7 +8,7 @@ import (
 	"net/http"
 )
 
-func SendMessage(userData LoginResponse, roomId string, content interface{}) error {
+func SendMessage(token Token, roomId string, content interface{}) error {
 	transactionId, err := uuid.NewRandom()
 	if err != nil {
 		return err
@@ -29,7 +29,7 @@ func SendMessage(userData LoginResponse, roomId string, content interface{}) err
 	if err != nil {
 		panic(err)
 	}
-	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", userData.AccessToken))
+	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
 	response, err := http.DefaultClient.Do(request)
 	if err != nil {
 		return err
diff --git a/api/setpusher.go b/api/setpusher.go
index 2766037d90b671565089012421edc5899188de34..c223f86c13876bc9c05474ca0f5d1b2c7c9fa5c3 100644
--- a/api/setpusher.go
+++ b/api/setpusher.go
@@ -26,7 +26,7 @@ type PusherData struct {
 	Url    string `json:"url"`
 }
 
-func SetPusher(userData LoginResponse, url string) error {
+func SetPusher(token Token, url string) error {
 	body, err := json.Marshal(PusherRequest{
 		AppDisplayName: "webhook",
 		AppId:          "de.justjanne.webhook",
@@ -52,7 +52,7 @@ func SetPusher(userData LoginResponse, url string) error {
 	if err != nil {
 		panic(err)
 	}
-	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", userData.AccessToken))
+	request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
 	response, err := http.DefaultClient.Do(request)
 	if err != nil {
 		return err
diff --git a/api/token.go b/api/token.go
new file mode 100644
index 0000000000000000000000000000000000000000..fa9c55954fdde2f1da8ce67522ebd5ef519d5d4b
--- /dev/null
+++ b/api/token.go
@@ -0,0 +1,9 @@
+package api
+
+import "time"
+
+type Token struct {
+	AccessToken  string
+	Expires      time.Time
+	RefreshToken string
+}
diff --git a/main.go b/main.go
index 1e1d5bb4b01dde647c03638ecaf45be3e10cd3a3..80556d900ebfa75515efba3af05458abc066ded9 100644
--- a/main.go
+++ b/main.go
@@ -13,15 +13,11 @@ import (
 )
 
 func main() {
-	bot, err := NewMatrixBot(
-		os.Getenv("BOT_USERNAME"),
-		os.Getenv("BOT_PASSWORD"),
-		os.Getenv("BOT_DEVICEID"),
-		os.Getenv("BOT_PUSHURL"),
-	)
-	if err != nil {
-		panic(err)
-	}
+	var err error
+
+	bot := NewMatrixBot()
+
+	// !8ball handler
 	bot.HandleFunc("!8ball", func(bot *MatrixBot, notification api.Notification) error {
 		positive := []string{
 			"It is certain",
@@ -64,12 +60,14 @@ func main() {
 		}
 		answer := answers[rand.IntN(len(answers))]
 
-		err = api.SendMessage(bot.userData, notification.RoomId, api.MessageContent{
+		err = api.SendMessage(*bot.token, notification.RoomId, api.MessageContent{
 			Body:    answer,
 			MsgType: "m.text",
 		})
 		return nil
 	})
+
+	// !trains handler
 	bahnTpl, err := template.New("bahn").Parse(`{{- /*gotype: bahn.Timetable*/ -}}
 <b>{{.Station}}</b> Abfahrten
 {{- range .Stops -}}
@@ -103,12 +101,20 @@ func main() {
 		if err != nil {
 			return err
 		}
-		err = api.SendMessage(bot.userData, notification.RoomId, api.MessageContent{
+		err = api.SendMessage(*bot.token, notification.RoomId, api.MessageContent{
 			FormattedBody: buf.String(),
 			Format:        "org.matrix.custom.html",
 			MsgType:       "m.text",
 		})
 		return nil
 	})
+	err = bot.Login(os.Getenv("BOT_USERNAME"), os.Getenv("BOT_PASSWORD"), os.Getenv("BOT_DEVICEID"))
+	if err != nil {
+		panic(err)
+	}
+	err = bot.RegisterPusher(os.Getenv("BOT_PUSHURL"))
+	if err != nil {
+		panic(err)
+	}
 	bot.Serve(":8080")
 }
diff --git a/matrixbot.go b/matrixbot.go
index 6538da0dc2254ec63dcbc70cf36a0e582ef71632..17580610e9a3a79297e553a3a586a11b497b45d1 100644
--- a/matrixbot.go
+++ b/matrixbot.go
@@ -1,36 +1,69 @@
 package main
 
 import (
+	"fmt"
 	"git.kuschku.de/justjanne/stateless-matrix-bot/api"
 	"io"
 	"log"
 	"net/http"
 	"strings"
+	"time"
 )
 
 type MatrixBot struct {
-	userData api.LoginResponse
+	token    *api.Token
 	handlers map[string]func(bot *MatrixBot, notification api.Notification) error
 }
 
-func NewMatrixBot(
-	username string,
-	password string,
-	deviceId string,
-	url string,
-) (*MatrixBot, error) {
-	userData, err := api.Login(username, password, deviceId)
+func NewMatrixBot() *MatrixBot {
+	return &MatrixBot{
+		token:    nil,
+		handlers: make(map[string]func(bot *MatrixBot, notification api.Notification) error),
+	}
+}
+
+func (bot *MatrixBot) RefreshToken() error {
+	if bot.token == nil {
+		return fmt.Errorf("no refresh token available")
+	}
+	userData, err := api.Refresh(bot.token.RefreshToken)
 	if err != nil {
-		return nil, err
+		return err
+	}
+	bot.token = &api.Token{
+		AccessToken:  userData.AccessToken,
+		Expires:      time.Now().Add(time.Duration(userData.ExpiresInMs) / 2 * time.Millisecond),
+		RefreshToken: userData.RefreshToken,
 	}
-	err = api.SetPusher(userData, url)
+	return nil
+}
+
+func (bot *MatrixBot) Login(username string, password string, deviceId string) error {
+	userData, err := api.Login(username, password, deviceId)
 	if err != nil {
-		return nil, err
+		return err
 	}
-	return &MatrixBot{
-		userData: userData,
-		handlers: make(map[string]func(bot *MatrixBot, notification api.Notification) error),
-	}, nil
+	bot.token = &api.Token{
+		AccessToken:  userData.AccessToken,
+		Expires:      time.Now().Add(time.Duration(userData.ExpiresInMs) / 2 * time.Millisecond),
+		RefreshToken: userData.RefreshToken,
+	}
+	return nil
+}
+
+func (bot *MatrixBot) RefreshTask() {
+	for true {
+		if bot.token != nil && time.Now().After(bot.token.Expires) {
+			if err := bot.RefreshToken(); err != nil {
+				log.Printf("error refresh token: %s\n", err.Error())
+			}
+		}
+		time.Sleep(1 * time.Second)
+	}
+}
+
+func (bot *MatrixBot) RegisterPusher(url string) error {
+	return api.SetPusher(*bot.token, url)
 }
 
 func (bot *MatrixBot) HandleFunc(command string, handler func(bot *MatrixBot, notification api.Notification) error) {
@@ -39,7 +72,7 @@ func (bot *MatrixBot) HandleFunc(command string, handler func(bot *MatrixBot, no
 
 func (bot *MatrixBot) Serve(endpoint string) {
 	http.HandleFunc("/healthz", func(writer http.ResponseWriter, request *http.Request) {
-		io.WriteString(writer, "OK\n")
+		_, _ = io.WriteString(writer, "OK\n")
 	})
 	http.HandleFunc("/_matrix/push/v1/notify", func(writer http.ResponseWriter, request *http.Request) {
 		notification, err := api.ParseNotification(request.Body)
@@ -50,7 +83,7 @@ func (bot *MatrixBot) Serve(endpoint string) {
 		if notification.EventId == "" {
 			return
 		}
-		err = api.SetReadReceipt(bot.userData, notification.RoomId, notification.EventId)
+		err = api.SetReadReceipt(*bot.token, notification.RoomId, notification.EventId)
 		if err != nil {
 			log.Println(err.Error())
 			return