diff --git a/api/api.go b/api/api.go
index b53a1ae74271850b955468bad734677580357be0..07679ce59ce3e31939ef8825714a448f34a8e616 100644
--- a/api/api.go
+++ b/api/api.go
@@ -21,7 +21,7 @@ func initDB(dbPath string) (*gorm.DB, error) {
 	}
 
 	db.AutoMigrate(&Member{}, &Product{}, &Purchase{}, &Topup{}, &Transaction{},
-		&OrderPurchase{}, &Order{})
+		&OrderPurchase{}, &Order{}, &PasswordReset{})
 	return db, err
 }
 
@@ -33,12 +33,16 @@ func Init(dbPath string, signKey string, mail *Mail, r *mux.Router) error {
 
 	a := api{db, []byte(signKey), mail}
 	go a.refundOrders()
+	go a.cleanPaswordResets()
 
 	token, err := a.newToken(0, "admin", false)
 	log.Print(token)
 
 	r.HandleFunc("/signin", a.SignIn).Methods("POST")
 	r.HandleFunc("/token", a.GetToken).Methods("GET")
+	r.HandleFunc("/reset", a.SendPasswordReset).Methods("POST")
+	r.HandleFunc("/reset/{token}", a.ValidatePasswordReset).Methods("GET")
+	r.HandleFunc("/reset/{token}", a.PasswordReset).Methods("PUT")
 
 	r.HandleFunc("/member", a.authAdmin(a.ListMembers)).Methods("GET")
 	r.HandleFunc("/member", a.authAdmin(a.AddMember)).Methods("POST")
diff --git a/api/auth.go b/api/auth.go
index b91f4c26a377fb3e2a58fd32cfc67a4da4e36b61..53de1d26f5c09980c3ddb2e25900a522c58306d2 100644
--- a/api/auth.go
+++ b/api/auth.go
@@ -3,21 +3,45 @@ package api
 import (
 	"crypto/rand"
 	"crypto/subtle"
+	"encoding/base64"
 	"encoding/json"
+	"errors"
 	"log"
 	"net/http"
+	"net/url"
 	"time"
 
 	"github.com/dgrijalva/jwt-go"
+	"github.com/gorilla/mux"
 	"golang.org/x/crypto/argon2"
+	"gorm.io/gorm"
 )
 
+const (
+	timeExpireResetToken = 2 * 24 * time.Hour
+)
+
+type PasswordReset struct {
+	gorm.Model
+	Token     string  `gorm:"unique;index"`
+	MemberNum int     `gorm:"column:member"`
+	Member    *Member `gorm:"foreignKey:MemberNum;references:Num"`
+}
+
 type creds struct {
 	Login    string `json:"login"`
 	Password string `json:"password"`
 	NoExpire bool   `json:"noExpire"`
 }
 
+type passwordResetPost struct {
+	Email string `json:"email"`
+}
+
+type passwordResetPut struct {
+	Password string `json:"password"`
+}
+
 func (a *api) SignIn(w http.ResponseWriter, req *http.Request) {
 	var c creds
 	err := json.NewDecoder(req.Body).Decode(&c)
@@ -95,6 +119,152 @@ func (a *api) GetToken(w http.ResponseWriter, req *http.Request) {
 	}
 }
 
+func (a *api) SendPasswordReset(w http.ResponseWriter, req *http.Request) {
+	var reset passwordResetPost
+	err := json.NewDecoder(req.Body).Decode(&reset)
+	if err != nil {
+		log.Printf("Can't decode password reset request: %v", err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+	var member Member
+	err = a.db.Where("email = ?", reset.Email).First(&member).Error
+	if err != nil {
+		log.Printf("Can't locate user %s: %v", reset.Email, err)
+		w.WriteHeader(http.StatusBadRequest)
+		return
+	}
+
+	tokenBytes := make([]byte, 15)
+	_, err = rand.Read(tokenBytes)
+	if err != nil {
+		log.Printf("Can't generate a random token for password reset: %v", err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+	token := base64.URLEncoding.EncodeToString(tokenBytes)
+	passwordReset := PasswordReset{
+		Token:     token,
+		MemberNum: member.Num,
+	}
+	url := url.URL{
+		Scheme: req.URL.Scheme,
+		Host:   req.URL.Host,
+		Path:   "/api/reset/" + token,
+	}
+	err = a.db.Transaction(func(tx *gorm.DB) error {
+		err = tx.Create(&passwordReset).Error
+		if err != nil {
+			return err
+		}
+		return a.mail.sendPasswordReset(member, url.String())
+	})
+	if err != nil {
+		log.Printf("Error sending password reset: %v", err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+	w.Write([]byte("Email sent"))
+	w.WriteHeader(http.StatusCreated)
+}
+
+func (a *api) ValidatePasswordReset(w http.ResponseWriter, req *http.Request) {
+	passwordReset, status := a.getPasswordReset(req)
+	if status != http.StatusOK {
+		w.WriteHeader(status)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	w.WriteHeader(http.StatusOK)
+	err := json.NewEncoder(w).Encode(passwordReset.Member)
+	if err != nil {
+		log.Printf("Can't encode reset member: %v", err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+}
+
+func (a *api) PasswordReset(w http.ResponseWriter, req *http.Request) {
+	passwordReset, status := a.getPasswordReset(req)
+	if status != http.StatusOK {
+		w.WriteHeader(status)
+		return
+	}
+	var reset passwordResetPut
+	err := json.NewDecoder(req.Body).Decode(&reset)
+	if err != nil {
+		log.Printf("Can't decode password reset put: %v", err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+	hash, salt, err := newHashPass(reset.Password)
+	if err != nil {
+		log.Printf("Can't hash password: %v", err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+
+	err = a.db.Transaction(func(tx *gorm.DB) error {
+		err := a.db.Model(&Member{}).
+			Updates(Member{
+				PassHash: hash,
+				Salt:     salt,
+			}).Error
+		if err != nil {
+			return err
+		}
+		return a.db.Delete(passwordReset).Error
+	})
+	if err != nil {
+		log.Printf("Error updating password: %v", err)
+		w.WriteHeader(http.StatusInternalServerError)
+		return
+	}
+	w.Write([]byte("Email sent"))
+	w.WriteHeader(http.StatusAccepted)
+}
+
+func (a *api) getPasswordReset(req *http.Request) (PasswordReset, int) {
+	vars := mux.Vars(req)
+	token := vars["token"]
+
+	var passwordReset PasswordReset
+	err := a.db.Where("token = ?", token).
+		Preload("Member").
+		First(&passwordReset).Error
+	status := http.StatusOK
+	if err != nil {
+		if errors.Is(err, gorm.ErrRecordNotFound) {
+			status = http.StatusNotFound
+		} else {
+			log.Printf("Can't get password reset %s: %v", token, err)
+			status = http.StatusInternalServerError
+		}
+	}
+	return passwordReset, status
+}
+
+func (a *api) cleanPaswordResets() {
+	time.Sleep(time.Minute)
+	const refundSleeptime = 10 * time.Minute
+	for {
+		time.Sleep(refundSleeptime)
+		a.cleanReset()
+	}
+}
+
+func (a *api) cleanReset() {
+	t := time.Now().Add(timeExpireResetToken)
+	res := a.db.Where("created_at < ?", true, t).
+		Delete(&PasswordReset{})
+	if res.Error != nil {
+		log.Println("Error deleting old reset tokens:", res.Error)
+	} else if res.RowsAffected != 0 {
+		log.Println("Deleted", res.RowsAffected, "password reset tokens")
+	}
+}
+
 func (a *api) auth(fn func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
 	return func(w http.ResponseWriter, req *http.Request) {
 		token := req.Header.Get("x-authentication")
@@ -223,7 +393,7 @@ func (a *api) validateToken(token string) (bool, jwt.MapClaims) {
 }
 
 func newHashPass(password string) (hash []byte, salt []byte, err error) {
-	salt = make([]byte, 32)
+	salt = make([]byte, 16)
 	_, err = rand.Read(salt)
 	if err != nil {
 		return
diff --git a/api/mail.go b/api/mail.go
index a4924d1585f049c09d9cf96451e0245b402a9d35..7d904a35205718ebd4dcd26748d2ef043ba44075 100644
--- a/api/mail.go
+++ b/api/mail.go
@@ -11,9 +11,11 @@ const (
 	orderTmpl = `To: {{.To}}
 From: {{.From}}
 Content-Type: text/plain; charset="utf-8"
-Subject: [garbanzo] pedido {{.Name}}
+Subject: [garbanzo] pedido {{.OrderName}}
 
-El pedido {{.Name}} a sido cerrado.
+Hola {{.MemberName}},
+
+El pedido {{.OrderName}} a sido cerrado.
 
 Se han pedido:{{range $name, $amount := .Products}}
 * {{$name}}: {{$amount}}{{end}}
@@ -23,6 +25,24 @@ Las siguientes personas han pedido:
   {{$name}}:{{range $purchases}}
     * {{.Product.Name}}: {{.Amount}}{{end}}
 {{end}}
+
+Salud y garbancicos.
+`
+	resetTmpl = `To: {{.To}}
+From: {{.From}}
+Content-Type: text/plain; charset="utf-8"
+Subject: [garbanzo] recupera tu contraseña
+
+Hola {{.Name}},
+
+Hemos recivido una petición para recuperar tu contraseña en el Garbanzo Negro.
+Si no has pedido cambiar tu contraseña ignora este email o si siguen llegandote
+emails como este informa a las administradoras.
+
+Para cambiar tu contraseña visita el siguiente enlace y sigue las instrucciones:
+{{.Link}}
+
+Salud y garbancicos.
 `
 )
 
@@ -46,11 +66,12 @@ func NewMail(email, password, server string) *Mail {
 }
 
 type orderData struct {
-	To        string
-	From      string
-	Name      string
-	Products  map[string]int
-	Purchases map[string][]OrderPurchase
+	To         string
+	From       string
+	MemberName string
+	OrderName  string
+	Products   map[string]int
+	Purchases  map[string][]OrderPurchase
 }
 
 func (m Mail) sendOrder(to string, order *Order) error {
@@ -72,11 +93,12 @@ func (m Mail) sendOrder(to string, order *Order) error {
 		purchases[t.Member.Name] = purchase
 	}
 	data := orderData{
-		To:        to,
-		From:      m.email,
-		Name:      order.Name,
-		Products:  products,
-		Purchases: purchases,
+		To:         to,
+		From:       m.email,
+		OrderName:  order.Name,
+		MemberName: order.Member.Name,
+		Products:   products,
+		Purchases:  purchases,
 	}
 
 	var buff bytes.Buffer
@@ -86,3 +108,30 @@ func (m Mail) sendOrder(to string, order *Order) error {
 	}
 	return smtp.SendMail(m.server, m.auth, m.email, []string{to}, buff.Bytes())
 }
+
+type passwordResetData struct {
+	To   string
+	From string
+	Name string
+	Link string
+}
+
+func (m Mail) sendPasswordReset(member Member, link string) error {
+	if m.server == "" {
+		return nil
+	}
+
+	var buff bytes.Buffer
+	data := passwordResetData{
+		To:   member.Email,
+		From: m.email,
+		Name: member.Name,
+		Link: link,
+	}
+
+	err := m.tmpl.ExecuteTemplate(&buff, "order", data)
+	if err != nil {
+		return err
+	}
+	return smtp.SendMail(m.server, m.auth, m.email, []string{member.Email}, buff.Bytes())
+}