diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..95d5bd78de01b48c1dac1bf08560a663b901105d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+*.iml
+/.idea
+/bin/
+/manifest.json
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..76dad46bbc7e776d92daa77d92451f8464adb605
--- /dev/null
+++ b/README.md
@@ -0,0 +1,31 @@
+## Usage
+
+Invoke binary (hetzner-robot-automation) with a config file as parameter.
+That config file should contain the API keys, server id, and list of files to upload and/or execute.
+
+The binary then reboots the specified server into rescue mode, copies the specified files over, and executes them if
+specified.
+
+The config should comply with the JSON schema and look similar to
+
+```json
+{
+  "api-key": "AzureDiamond",
+  "api-secret": "hunter2",
+  "server": 123456,
+  "files": [
+    {
+      "source": "config/ignition.ign",
+      "target": "/root/ignition.ign",
+      "mode": 644,
+      "execute": false
+    },
+    {
+      "source": "install.sh",
+      "target": "/root/install_rescue.sh",
+      "mode": 755,
+      "execute": true
+    }
+  ]
+}
+```
\ No newline at end of file
diff --git a/cli/deploy.go b/cli/deploy.go
new file mode 100644
index 0000000000000000000000000000000000000000..0cac6dc2824df5f9309e31e96bc4f3c8868f0ffe
--- /dev/null
+++ b/cli/deploy.go
@@ -0,0 +1,59 @@
+package main
+
+import (
+	"fmt"
+	"golang.org/x/crypto/ssh"
+	"io"
+	"log"
+)
+
+func isExitMissingError(err error) bool {
+	_, isExitMissingError := err.(*ssh.ExitMissingError)
+	return isExitMissingError
+}
+
+func deploy(manifest Manifest) error {
+	connectionData, err := provideConnection(manifest)
+	if err != nil {
+		return fmt.Errorf("error while obtaining connection data: %w", err)
+	}
+	sshClient, err := openSshClient(connectionData)
+	if err != nil {
+		return fmt.Errorf("error while opening ssh client: %w", err)
+	}
+	defer sshClient.Close()
+
+	for _, file := range manifest.Files {
+		sshSession, err := openSshSession(sshClient)
+		if err != nil {
+			return fmt.Errorf("error while opening ssh session: %w", err)
+		}
+		log.Printf("copying file %s to %s with mode 0%d \n", file.Source, file.Target, file.Mode)
+		if err := copyFile(sshSession, file); err != nil && !isExitMissingError(err) {
+			_ = sshSession.Close()
+			return fmt.Errorf("error while copying file %s: %w", file.Target, err)
+		}
+		if err := sshSession.Close(); err != nil && err != io.EOF {
+			return fmt.Errorf("error while closing ssh session: %w", err)
+		}
+	}
+
+	for _, file := range manifest.Files {
+		if file.Execute {
+			sshSession, err := openSshSession(sshClient)
+			if err != nil {
+				return fmt.Errorf("error while opening ssh session: %w", err)
+			}
+			log.Printf("running command %s\n", file.Target)
+			if err := sshSession.Run(file.Target); err != nil && !isExitMissingError(err) {
+				_ = sshSession.Close()
+				return fmt.Errorf("error while running command: %w", err)
+			}
+			if err := sshSession.Close(); err != nil && err != io.EOF {
+				return fmt.Errorf("error while closing ssh session: %w", err)
+			}
+		}
+	}
+
+	return nil
+}
diff --git a/cli/hetzner.go b/cli/hetzner.go
new file mode 100644
index 0000000000000000000000000000000000000000..15bbcde75954da9867843b739516c6410d1dd974
--- /dev/null
+++ b/cli/hetzner.go
@@ -0,0 +1,57 @@
+package main
+
+import (
+	"fmt"
+	"hetzner-robot/client"
+	"hetzner-robot/models"
+	"log"
+)
+
+func enableRescue(robotClient client.RobotClient, server models.ServerNumber) (*SshConnection, error) {
+	oldRescueInfo, err := robotClient.BootRescueGetLast(server)
+	if err != nil {
+		return nil, fmt.Errorf("error while retrieving rescue status: %w", err)
+	}
+	if oldRescueInfo.Active {
+		log.Println("rescue system already active, deactivating")
+		if err := robotClient.BootRescueDelete(server); err != nil {
+			return nil, fmt.Errorf("error while deactivating rescue system: %w", err)
+		}
+	}
+
+	log.Println("activating rescue system")
+	rescueInfo, err := robotClient.BootRescueSet(server, &models.RescueSetInput{
+		OS: "linux",
+	})
+	if err != nil {
+		return nil, fmt.Errorf("error while setting server rescue properties: %w", err)
+	}
+
+	return &SshConnection{
+		Username: "root",
+		Password: rescueInfo.Password,
+		Hostname: rescueInfo.ServerIP,
+		Port:     22,
+	}, nil
+}
+
+func provideConnection(manifest Manifest) (*SshConnection, error) {
+	robotClient := client.NewBasicAuthClient(manifest.ApiKey, manifest.ApiSecret)
+
+	server, err := robotClient.ServerGet(models.ServerNumber(manifest.Server))
+	if err != nil {
+		return nil, fmt.Errorf("error while retrieving server: %w", err)
+	}
+	connectionData, err := enableRescue(robotClient, server.ServerNumber)
+	if err != nil {
+		return nil, fmt.Errorf("error deploying configuration to server: %w", err)
+	}
+	log.Println("restarting server")
+	_, err = robotClient.ResetSet(server.ServerNumber, &models.ResetSetInput{
+		Type: models.ResetTypeHardware,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("error restarting server: %w", err)
+	}
+	return connectionData, nil
+}
diff --git a/cli/main.go b/cli/main.go
new file mode 100644
index 0000000000000000000000000000000000000000..09312526ff804dff58d92f671e5b9b5d1e90e3fc
--- /dev/null
+++ b/cli/main.go
@@ -0,0 +1,65 @@
+package main
+
+import (
+	"encoding/json"
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+)
+
+type Manifest struct {
+	ApiKey    string     `json:"api-key"`
+	ApiSecret string     `json:"api-secret"`
+	Server    int        `json:"server"`
+	Files     []CopyFile `json:"files"`
+}
+
+type CopyFile struct {
+	Source  string `json:"source"`
+	Target  string `json:"target"`
+	Mode    uint   `json:"mode"`
+	Execute bool   `json:"execute"`
+}
+
+func (manifest *Manifest) resolvePaths(root string) {
+	for i, file := range manifest.Files {
+		if !filepath.IsAbs(file.Source) {
+			newPath := filepath.Join(root, file.Source)
+			manifest.Files[i].Source = newPath
+		}
+	}
+}
+
+func loadManifest(filename string) (Manifest, error) {
+	var manifest Manifest
+	manifestPath, err := filepath.Abs(filename)
+	if err != nil {
+		return manifest, fmt.Errorf("could not find absolute path for file")
+	}
+	manifestFile, err := os.Open(manifestPath)
+	if err != nil {
+		return manifest, fmt.Errorf("error opening manifest: %w", err)
+	}
+	if err := json.NewDecoder(manifestFile).Decode(&manifest); err != nil {
+		return manifest, fmt.Errorf("error loading manifest: %w", err)
+	}
+	manifest.resolvePaths(filepath.Dir(manifestPath))
+	return manifest, nil
+}
+
+func main() {
+	flag.Parse()
+	for _, arg := range flag.Args() {
+		log.Printf("loading manifest %s", arg)
+		manifest, err := loadManifest(arg)
+		if err != nil {
+			log.Fatalf(err.Error())
+		}
+		err = deploy(manifest)
+		if err != nil {
+			log.Fatalf(err.Error())
+		}
+	}
+}
diff --git a/cli/ssh.go b/cli/ssh.go
new file mode 100644
index 0000000000000000000000000000000000000000..27c2138f56ca5c45e358b2a8cbdc5216709db004
--- /dev/null
+++ b/cli/ssh.go
@@ -0,0 +1,140 @@
+package main
+
+import (
+	"bufio"
+	"fmt"
+	"golang.org/x/crypto/ssh"
+	"golang.org/x/sync/errgroup"
+	"io"
+	"log"
+	"os"
+	"path"
+	"time"
+)
+
+type SshConnection struct {
+	Username string
+	Password string
+	Hostname string
+	Port     int
+	Timeout  time.Duration
+}
+
+func openSshClient(connection *SshConnection) (*ssh.Client, error) {
+	conf := &ssh.ClientConfig{
+		User:            connection.Username,
+		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+		Auth: []ssh.AuthMethod{
+			ssh.Password(connection.Password),
+		},
+		Timeout: connection.Timeout,
+	}
+
+	log.Println("opening ssh connection")
+	var conn *ssh.Client
+	for attempt := 0; attempt < 20; attempt++ {
+		if conn != nil {
+			if err := conn.Close(); err != nil {
+				return nil, fmt.Errorf("could not connect to server via ssh %w", err)
+			}
+		}
+		conn, err := ssh.Dial("tcp", fmt.Sprintf("%s:%d", connection.Hostname, connection.Port), conf)
+		if err == nil {
+			return conn, nil
+		}
+		log.Println("waiting on ssh session")
+		time.Sleep(5 * time.Second)
+	}
+	return nil, fmt.Errorf("could not connect to server via ssh")
+}
+
+func openSshSession(conn *ssh.Client) (*ssh.Session, error) {
+	log.Println("opening ssh session")
+	var session *ssh.Session
+	session, err := conn.NewSession()
+	if err != nil {
+		return nil, fmt.Errorf("error opening ssh session %w", err)
+	}
+
+	log.Println("connecting ssh outputs to stdout")
+	if err := connectShellOutputs(session); err != nil {
+		return nil, fmt.Errorf("error connecting ssh outputs to stdout %w", err)
+	}
+
+	return session, nil
+}
+
+func copyFile(session *ssh.Session, file CopyFile) error {
+	handle, err := os.Open(file.Source)
+	if err != nil {
+		return err
+	}
+	defer handle.Close()
+
+	stat, err := handle.Stat()
+	if err != nil {
+		return err
+	}
+
+	filedir, filename := path.Split(file.Target)
+
+	group := errgroup.Group{}
+	group.Go(func() error {
+		stdin, _ := session.StdinPipe()
+		defer stdin.Close()
+		if _, err := fmt.Fprintf(stdin, "C0%d %d %s\n", file.Mode, stat.Size(), filename); err != nil {
+			return fmt.Errorf("error sending file: %w", err)
+		}
+		if _, err := io.Copy(stdin, handle); err != nil {
+			return fmt.Errorf("error sending file: %w", err)
+		}
+		if _, err := fmt.Fprint(stdin, "\x00"); err != nil {
+			return fmt.Errorf("error sending file: %w", err)
+		}
+		return nil
+	})
+	group.Go(func() error {
+		if err := session.Run(fmt.Sprintf("/usr/bin/scp -t %s", filedir)); err != nil {
+			return fmt.Errorf("error receiving file: %w", err)
+		}
+		return nil
+	})
+	return group.Wait()
+}
+
+func connectReader(label string, reader io.Reader) {
+	scanner := bufio.NewScanner(reader)
+	for {
+		if tkn := scanner.Scan(); tkn {
+			rcv := scanner.Bytes()
+			raw := make([]byte, len(rcv))
+			copy(raw, rcv)
+			log.Printf("%s: %s\n", label, string(raw))
+		} else {
+			if scanner.Err() != nil {
+				log.Printf("%s error: %s\n", label, scanner.Err())
+			}
+			return
+		}
+	}
+}
+
+func connectShellOutputs(session *ssh.Session) error {
+	var stdout, stderr io.Reader
+	var err error
+
+	stdout, err = session.StdoutPipe()
+	if err != nil {
+		return err
+	}
+
+	stderr, err = session.StderrPipe()
+	if err != nil {
+		return err
+	}
+
+	go connectReader("ssh stdout", stdout)
+	go connectReader("ssh stderr", stderr)
+
+	return nil
+}
diff --git a/client/boot.go b/client/boot.go
new file mode 100644
index 0000000000000000000000000000000000000000..c37f834acbe28a3d5446e75c167defeb588f16cb
--- /dev/null
+++ b/client/boot.go
@@ -0,0 +1,82 @@
+package client
+
+import (
+	"encoding/json"
+	"fmt"
+	"hetzner-robot/models"
+	"net/http"
+	neturl "net/url"
+	"strconv"
+)
+
+func (c *Client) BootRescueGet(server models.ServerNumber) (*models.RescueInfo, error) {
+	url := fmt.Sprintf(c.baseURL+"/boot/%d/rescue", int(server))
+	bytes, err := c.doGetRequest(url)
+	if err != nil {
+		return nil, err
+	}
+
+	var rescueResp models.RescueInfoGetResponse
+	err = json.Unmarshal(bytes, &rescueResp)
+	if err != nil {
+		return nil, err
+	}
+
+	return &rescueResp.Rescue, nil
+}
+
+func (c *Client) BootRescueGetLast(server models.ServerNumber) (*models.Rescue, error) {
+	url := fmt.Sprintf(c.baseURL+"/boot/%d/rescue/last", int(server))
+	bytes, err := c.doGetRequest(url)
+	if err != nil {
+		return nil, err
+	}
+
+	var rescueResp models.RescueGetResponse
+	err = json.Unmarshal(bytes, &rescueResp)
+	if err != nil {
+		return nil, err
+	}
+
+	return &rescueResp.Rescue, nil
+}
+
+func (c *Client) BootRescueSet(server models.ServerNumber, input *models.RescueSetInput) (*models.Rescue, error) {
+	url := fmt.Sprintf(c.baseURL+"/boot/%d/rescue", int(server))
+
+	formData := neturl.Values{}
+	formData.Set("os", input.OS)
+	if input.Arch > 0 {
+		formData.Set("arch", strconv.Itoa(input.Arch))
+	}
+	if len(input.AuthorizedKeys) > 0 {
+		for _, authorizedKey := range input.AuthorizedKeys {
+			formData.Add("authorized_key[]", authorizedKey.Fingerprint)
+		}
+	}
+
+	bytes, err := c.doPostFormRequest(url, formData)
+	if err != nil {
+		return nil, err
+	}
+
+	var rescueResp models.RescueGetResponse
+	err = json.Unmarshal(bytes, &rescueResp)
+	if err != nil {
+		return nil, err
+	}
+
+	return &rescueResp.Rescue, nil
+}
+
+func (c *Client) BootRescueDelete(server models.ServerNumber) error {
+	url := fmt.Sprintf(c.baseURL+"/boot/%d/rescue", int(server))
+
+	req, err := http.NewRequest("DELETE", url, nil)
+	if err != nil {
+		return err
+	}
+
+	_, err = c.doRequest(req)
+	return err
+}
diff --git a/client/client.go b/client/client.go
new file mode 100644
index 0000000000000000000000000000000000000000..334022a4a2b7d68dd5ca6e8f980f61302787cebe
--- /dev/null
+++ b/client/client.go
@@ -0,0 +1,89 @@
+package client
+
+import (
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+const baseURL string = "https://robot-ws.your-server.de"
+const version = "0.1.3"
+const userAgent = "hrobot-client/" + version
+
+type Client struct {
+	Username  string
+	Password  string
+	baseURL   string
+	userAgent string
+}
+
+func NewBasicAuthClient(username, password string) RobotClient {
+	return &Client{
+		Username:  username,
+		Password:  password,
+		baseURL:   baseURL,
+		userAgent: userAgent,
+	}
+}
+
+func (c *Client) SetBaseURL(baseURL string) {
+	c.baseURL = baseURL
+}
+
+func (c *Client) SetUserAgent(userAgent string) {
+	c.userAgent = userAgent
+}
+
+func (c *Client) GetVersion() string {
+	return version
+}
+
+func (c *Client) doGetRequest(url string) ([]byte, error) {
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	bytes, err := c.doRequest(req)
+	if err != nil {
+		return nil, err
+	}
+
+	return bytes, nil
+}
+
+func (c *Client) doPostFormRequest(url string, formData url.Values) ([]byte, error) {
+	req, err := http.NewRequest("POST", url, strings.NewReader(formData.Encode()))
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+	bytes, err := c.doRequest(req)
+	if err != nil {
+		return nil, err
+	}
+
+	return bytes, nil
+}
+
+func (c *Client) doRequest(req *http.Request) ([]byte, error) {
+	req.Header.Set("User-Agent", c.userAgent)
+	req.SetBasicAuth(c.Username, c.Password)
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return nil, err
+	}
+	if 200 != resp.StatusCode {
+		return nil, fmt.Errorf("%s", body)
+	}
+	return body, nil
+}
diff --git a/client/failover.go b/client/failover.go
new file mode 100644
index 0000000000000000000000000000000000000000..a362a7daf58ddb82287f093e12a094dd2e91a228
--- /dev/null
+++ b/client/failover.go
@@ -0,0 +1,44 @@
+package client
+
+import (
+	"encoding/json"
+	"fmt"
+	"hetzner-robot/models"
+)
+
+func (c *Client) FailoverGetList() ([]models.Failover, error) {
+	url := c.baseURL + "/failover"
+	bytes, err := c.doGetRequest(url)
+	if err != nil {
+		return nil, err
+	}
+
+	var failoverList []models.FailoverResponse
+	err = json.Unmarshal(bytes, &failoverList)
+	if err != nil {
+		return nil, err
+	}
+
+	var data []models.Failover
+	for _, failover := range failoverList {
+		data = append(data, failover.Failover)
+	}
+
+	return data, nil
+}
+
+func (c *Client) FailoverGet(server models.ServerNumber) (*models.Failover, error) {
+	url := fmt.Sprintf(c.baseURL+"/failover/%d", int(server))
+	bytes, err := c.doGetRequest(url)
+	if err != nil {
+		return nil, err
+	}
+
+	var failoverResp models.FailoverResponse
+	err = json.Unmarshal(bytes, &failoverResp)
+	if err != nil {
+		return nil, err
+	}
+
+	return &failoverResp.Failover, nil
+}
diff --git a/client/interface.go b/client/interface.go
new file mode 100644
index 0000000000000000000000000000000000000000..5d38c27bc796182cc210d1be6df4cf4d3950d4f2
--- /dev/null
+++ b/client/interface.go
@@ -0,0 +1,27 @@
+package client
+
+import (
+	"hetzner-robot/models"
+)
+
+type RobotClient interface {
+	SetBaseURL(baseURL string)
+	SetUserAgent(userAgent string)
+	GetVersion() string
+	ServerGetList() ([]models.Server, error)
+	ServerGet(server models.ServerNumber) (*models.Server, error)
+	ServerSetName(server models.ServerNumber, input *models.ServerSetNameInput) (*models.Server, error)
+	ServerReverse(server models.ServerNumber) (*models.Cancellation, error)
+	KeyGetList() ([]models.Key, error)
+	IPGetList() ([]models.IP, error)
+	RDnsGetList() ([]models.Rdns, error)
+	RDnsGet(server models.ServerNumber) (*models.Rdns, error)
+	BootRescueGet(server models.ServerNumber) (*models.RescueInfo, error)
+	BootRescueGetLast(server models.ServerNumber) (*models.Rescue, error)
+	BootRescueSet(server models.ServerNumber, input *models.RescueSetInput) (*models.Rescue, error)
+	BootRescueDelete(server models.ServerNumber) error
+	ResetGet(server models.ServerNumber) (*models.Reset, error)
+	ResetSet(server models.ServerNumber, input *models.ResetSetInput) (*models.ResetPost, error)
+	FailoverGetList() ([]models.Failover, error)
+	FailoverGet(server models.ServerNumber) (*models.Failover, error)
+}
diff --git a/client/ip.go b/client/ip.go
new file mode 100644
index 0000000000000000000000000000000000000000..d130a0b6928dc809027811314e2a5d66986c0b18
--- /dev/null
+++ b/client/ip.go
@@ -0,0 +1,27 @@
+package client
+
+import (
+	"encoding/json"
+	"hetzner-robot/models"
+)
+
+func (c *Client) IPGetList() ([]models.IP, error) {
+	url := c.baseURL + "/ip"
+	bytes, err := c.doGetRequest(url)
+	if err != nil {
+		return nil, err
+	}
+
+	var ips []models.IPResponse
+	err = json.Unmarshal(bytes, &ips)
+	if err != nil {
+		return nil, err
+	}
+
+	var data []models.IP
+	for _, ip := range ips {
+		data = append(data, ip.IP)
+	}
+
+	return data, nil
+}
diff --git a/client/key.go b/client/key.go
new file mode 100644
index 0000000000000000000000000000000000000000..33124b9af54c2edb2ac12a41f6b6656755bba0a5
--- /dev/null
+++ b/client/key.go
@@ -0,0 +1,27 @@
+package client
+
+import (
+	"encoding/json"
+	"hetzner-robot/models"
+)
+
+func (c *Client) KeyGetList() ([]models.Key, error) {
+	url := c.baseURL + "/key"
+	bytes, err := c.doGetRequest(url)
+	if err != nil {
+		return nil, err
+	}
+
+	var keys []models.KeyResponse
+	err = json.Unmarshal(bytes, &keys)
+	if err != nil {
+		return nil, err
+	}
+
+	var data []models.Key
+	for _, key := range keys {
+		data = append(data, key.Key)
+	}
+
+	return data, nil
+}
diff --git a/client/rdns.go b/client/rdns.go
new file mode 100644
index 0000000000000000000000000000000000000000..1190c16fc6c65d8ebf0e7700f55fd051a166f14c
--- /dev/null
+++ b/client/rdns.go
@@ -0,0 +1,44 @@
+package client
+
+import (
+	"encoding/json"
+	"fmt"
+	"hetzner-robot/models"
+)
+
+func (c *Client) RDnsGetList() ([]models.Rdns, error) {
+	url := c.baseURL + "/rdns"
+	bytes, err := c.doGetRequest(url)
+	if err != nil {
+		return nil, err
+	}
+
+	var rdnsList []models.RdnsResponse
+	err = json.Unmarshal(bytes, &rdnsList)
+	if err != nil {
+		return nil, err
+	}
+
+	var data []models.Rdns
+	for _, rdns := range rdnsList {
+		data = append(data, rdns.Rdns)
+	}
+
+	return data, nil
+}
+
+func (c *Client) RDnsGet(server models.ServerNumber) (*models.Rdns, error) {
+	url := fmt.Sprintf(c.baseURL+"/rdns/%d", int(server))
+	bytes, err := c.doGetRequest(url)
+	if err != nil {
+		return nil, err
+	}
+
+	var rDnsResp models.RdnsResponse
+	err = json.Unmarshal(bytes, &rDnsResp)
+	if err != nil {
+		return nil, err
+	}
+
+	return &rDnsResp.Rdns, nil
+}
diff --git a/client/reset.go b/client/reset.go
new file mode 100644
index 0000000000000000000000000000000000000000..9949a1cdaf88e8f5cb7b65bafc112a16cad9a019
--- /dev/null
+++ b/client/reset.go
@@ -0,0 +1,44 @@
+package client
+
+import (
+	"encoding/json"
+	"fmt"
+	"hetzner-robot/models"
+	neturl "net/url"
+)
+
+func (c *Client) ResetGet(server models.ServerNumber) (*models.Reset, error) {
+	url := fmt.Sprintf(c.baseURL+"/reset/%d", int(server))
+	bytes, err := c.doGetRequest(url)
+	if err != nil {
+		return nil, err
+	}
+
+	var resetResp models.ResetResponse
+	err = json.Unmarshal(bytes, &resetResp)
+	if err != nil {
+		return nil, err
+	}
+
+	return &resetResp.Reset, nil
+}
+
+func (c *Client) ResetSet(server models.ServerNumber, input *models.ResetSetInput) (*models.ResetPost, error) {
+	url := fmt.Sprintf(c.baseURL+"/reset/%d", int(server))
+
+	formData := neturl.Values{}
+	formData.Set("type", input.Type)
+
+	bytes, err := c.doPostFormRequest(url, formData)
+	if err != nil {
+		return nil, err
+	}
+
+	var resetResp models.ResetPostResponse
+	err = json.Unmarshal(bytes, &resetResp)
+	if err != nil {
+		return nil, err
+	}
+
+	return &resetResp.Reset, nil
+}
diff --git a/client/server.go b/client/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..a455ad3f13191356df908960010c9b238ce37b86
--- /dev/null
+++ b/client/server.go
@@ -0,0 +1,82 @@
+package client
+
+import (
+	"encoding/json"
+	"fmt"
+	"hetzner-robot/models"
+	neturl "net/url"
+)
+
+func (c *Client) ServerGetList() ([]models.Server, error) {
+	url := c.baseURL + "/server"
+	bytes, err := c.doGetRequest(url)
+	if err != nil {
+		return nil, err
+	}
+
+	var servers []models.ServerResponse
+	err = json.Unmarshal(bytes, &servers)
+	if err != nil {
+		return nil, err
+	}
+
+	var data []models.Server
+	for _, server := range servers {
+		data = append(data, server.Server)
+	}
+
+	return data, nil
+}
+
+func (c *Client) ServerGet(server models.ServerNumber) (*models.Server, error) {
+	url := fmt.Sprintf(c.baseURL+"/server/%d", int(server))
+	bytes, err := c.doGetRequest(url)
+	if err != nil {
+		return nil, err
+	}
+
+	var serverResp models.ServerResponse
+	err = json.Unmarshal(bytes, &serverResp)
+	if err != nil {
+		return nil, err
+	}
+
+	return &serverResp.Server, nil
+}
+
+func (c *Client) ServerSetName(server models.ServerNumber, input *models.ServerSetNameInput) (*models.Server, error) {
+	url := fmt.Sprintf(c.baseURL+"/server/%d", int(server))
+
+	formData := neturl.Values{}
+	formData.Set("server_name", input.Name)
+
+	bytes, err := c.doPostFormRequest(url, formData)
+	if err != nil {
+		return nil, err
+	}
+
+	var serverResp models.ServerResponse
+	err = json.Unmarshal(bytes, &serverResp)
+	if err != nil {
+		return nil, err
+	}
+
+	return &serverResp.Server, nil
+}
+
+func (c *Client) ServerReverse(server models.ServerNumber) (*models.Cancellation, error) {
+	url := fmt.Sprintf(c.baseURL+"/server/%d/reversal", int(server))
+
+	bytes, err := c.doPostFormRequest(url, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	var cancelResp models.CancellationResponse
+	err = json.Unmarshal(bytes, &cancelResp)
+	if err != nil {
+		return nil, err
+	}
+
+	return &cancelResp.Cancellation, nil
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000000000000000000000000000000000000..27f9005f5dfdaa066cd6fbdf494984e3ba7e083c
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,10 @@
+module hetzner-robot-automation
+
+go 1.18
+
+require (
+	golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
+	golang.org/x/sync v0.0.0-20190423024810-112230192c58
+)
+
+require golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000000000000000000000000000000000000..fc390d9561570e3b1cbc2e8c3a7561d94872f825
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,7 @@
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d h1:sK3txAijHtOK88l68nt020reeT1ZdKLIYetKl95FzVY=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
+golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
diff --git a/models/boot.go b/models/boot.go
new file mode 100644
index 0000000000000000000000000000000000000000..be2bdd42339a03fb8ca0cdb1c3ac4cf2e047fb57
--- /dev/null
+++ b/models/boot.go
@@ -0,0 +1,41 @@
+package models
+
+type RescueInfoGetResponse struct {
+	Rescue RescueInfo `json:"rescue"`
+}
+
+type RescueGetResponse struct {
+	Rescue Rescue `json:"rescue"`
+}
+
+type AuthorizedKey struct {
+	Key Key `json:"key"`
+}
+
+type RescueInfo struct {
+	ServerIP      string          `json:"server_ip"`
+	ServerNumber  ServerNumber    `json:"server_number"`
+	Os            []string        `json:"os"`
+	Arch          []int           `json:"arch"`
+	Active        bool            `json:"active"`
+	Password      string          `json:"password"`
+	AuthorizedKey []AuthorizedKey `json:"authorized_key"`
+	HostKey       []interface{}   `json:"host_key"`
+}
+
+type Rescue struct {
+	ServerIP      string          `json:"server_ip"`
+	ServerNumber  ServerNumber    `json:"server_number"`
+	Os            string          `json:"os"`
+	Arch          int             `json:"arch"`
+	Active        bool            `json:"active"`
+	Password      string          `json:"password"`
+	AuthorizedKey []AuthorizedKey `json:"authorized_key"`
+	HostKey       []interface{}   `json:"host_key"`
+}
+
+type RescueSetInput struct {
+	OS             string
+	Arch           int
+	AuthorizedKeys []Key
+}
diff --git a/models/cancellation.go b/models/cancellation.go
new file mode 100644
index 0000000000000000000000000000000000000000..65d496b2009888a8686dd3c91e6d2067f75550a9
--- /dev/null
+++ b/models/cancellation.go
@@ -0,0 +1,14 @@
+package models
+
+type CancellationResponse struct {
+	Cancellation Cancellation `json:"cancellation"`
+}
+type Cancellation struct {
+	ServerIP                 string       `json:"server_ip"`
+	ServerNumber             ServerNumber `json:"server_number"`
+	ServerName               string       `json:"server_name"`
+	EarliestCancellationDate string       `json:"earliest_cancellation_date"`
+	Cancelled                bool         `json:"cancelled"`
+	CancellationDate         string       `json:"cancellation_date"`
+	CancellationReason       interface{}  `json:"cancellation_reason"`
+}
diff --git a/models/failover.go b/models/failover.go
new file mode 100644
index 0000000000000000000000000000000000000000..65ee63d869a423e2c8b49f485f514f750825e5f7
--- /dev/null
+++ b/models/failover.go
@@ -0,0 +1,13 @@
+package models
+
+type FailoverResponse struct {
+	Failover Failover `json:"failover"`
+}
+
+type Failover struct {
+	IP             string       `json:"ip"`
+	Netmask        string       `json:"netmask"`
+	ServerIP       string       `json:"server_ip"`
+	ServerNumber   ServerNumber `json:"server_number"`
+	ActiveServerIP string       `json:"active_server_ip"`
+}
diff --git a/models/ip.go b/models/ip.go
new file mode 100644
index 0000000000000000000000000000000000000000..b349f1f35238a6b6ce3a5f5125ecbdbd1c925245
--- /dev/null
+++ b/models/ip.go
@@ -0,0 +1,17 @@
+package models
+
+type IPResponse struct {
+	IP IP `json:"ip"`
+}
+
+type IP struct {
+	IP              string `json:"ip"`
+	ServerIP        string `json:"server_ip"`
+	ServerNumber    int    `json:"server_number"`
+	Locked          bool   `json:"locked"`
+	SeparateMac     string `json:"separate_mac"`
+	TrafficWarnings bool   `json:"traffic_warnings"`
+	TrafficHourly   int    `json:"traffic_hourly"`
+	TrafficDaily    int    `json:"traffic_daily"`
+	TrafficMonthly  int    `json:"traffic_monthly"`
+}
diff --git a/models/key.go b/models/key.go
new file mode 100644
index 0000000000000000000000000000000000000000..ef59a9006a39832370aa4c333ae6641f51701e6c
--- /dev/null
+++ b/models/key.go
@@ -0,0 +1,13 @@
+package models
+
+type KeyResponse struct {
+	Key Key `json:"key"`
+}
+
+type Key struct {
+	Name        string `json:"name"`
+	Fingerprint string `json:"fingerprint"`
+	Type        string `json:"type"`
+	Size        int    `json:"size"`
+	Data        string `json:"data"`
+}
diff --git a/models/rdns.go b/models/rdns.go
new file mode 100644
index 0000000000000000000000000000000000000000..6b191141c4d9ab40ced3983fdfcc18c3a6e475bb
--- /dev/null
+++ b/models/rdns.go
@@ -0,0 +1,10 @@
+package models
+
+type RdnsResponse struct {
+	Rdns Rdns `json:"rdns"`
+}
+
+type Rdns struct {
+	IP  string `json:"ip"`
+	Ptr string `json:"ptr"`
+}
diff --git a/models/reset.go b/models/reset.go
new file mode 100644
index 0000000000000000000000000000000000000000..6675e95e885d3db7f79b90f6b3c920f898bf4bac
--- /dev/null
+++ b/models/reset.go
@@ -0,0 +1,30 @@
+package models
+
+const ResetTypePower = "power"
+const ResetTypeHardware = "hw"
+const ResetTypeSoftware = "sw"
+const ResetTypeManual = "man"
+
+type ResetResponse struct {
+	Reset Reset `json:"reset"`
+}
+
+type Reset struct {
+	OperatingStatus string       `json:"operating_status"`
+	ServerIP        string       `json:"server_ip"`
+	ServerNumber    ServerNumber `json:"server_number"`
+	Type            []string     `json:"type"`
+}
+
+type ResetPostResponse struct {
+	Reset ResetPost `json:"reset"`
+}
+
+type ResetPost struct {
+	ServerIP string `json:"server_ip"`
+	Type     string `json:"type"`
+}
+
+type ResetSetInput struct {
+	Type string
+}
diff --git a/models/server.go b/models/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..647653a259e0cc3a11e889284808b9f856121224
--- /dev/null
+++ b/models/server.go
@@ -0,0 +1,40 @@
+package models
+
+type ServerResponse struct {
+	Server Server `json:"server"`
+}
+
+type Subnet struct {
+	IP   string `json:"ip"`
+	Mask string `json:"mask"`
+}
+
+type ServerNumber int
+
+type Server struct {
+	ServerIP     string       `json:"server_ip"`
+	ServerNumber ServerNumber `json:"server_number"`
+	ServerName   string       `json:"server_name"`
+	Product      string       `json:"product"`
+	Dc           string       `json:"dc"`
+	Traffic      string       `json:"traffic"`
+	Flatrate     bool         `json:"flatrate"`
+	Status       string       `json:"status"`
+	Throttled    bool         `json:"throttled"`
+	Cancelled    bool         `json:"cancelled"`
+	PaidUntil    string       `json:"paid_until"`
+	IP           []string     `json:"ip"`
+	Subnet       []Subnet     `json:"subnet"`
+	Reset        bool         `json:"reset"`
+	Rescue       bool         `json:"rescue"`
+	Vnc          bool         `json:"vnc"`
+	Windows      bool         `json:"windows"`
+	Plesk        bool         `json:"plesk"`
+	Cpanel       bool         `json:"cpanel"`
+	Wol          bool         `json:"wol"`
+	HotSwap      bool         `json:"hot_swap"`
+}
+
+type ServerSetNameInput struct {
+	Name string
+}
diff --git a/schema.json b/schema.json
new file mode 100644
index 0000000000000000000000000000000000000000..8f277d0dd2f66e8c899fa5129405e241e02c9c23
--- /dev/null
+++ b/schema.json
@@ -0,0 +1,50 @@
+{
+  "$schema": "https://json-schema.org/draft/2020-12/schema",
+  "$id": "https://git.kuschku.de/justjanne/hetzner-robot/schema.json",
+  "title": "hetzner robot manifest",
+  "description": "something here",
+  "type": "object",
+  "properties": {
+    "api-key": {
+      "description": "API Key for Hetzner Robot API (username)",
+      "type": "string"
+    },
+    "api-secret": {
+      "description": "API Secret for Hetzner Robot API (password)",
+      "type": "string"
+    },
+    "server": {
+      "description": "Hetzner Baremetal Server ID",
+      "type": "integer",
+      "exclusiveMinimum": 0
+    },
+    "files": {
+      "description": "Files to upload and execute",
+      "type": "array",
+      "items": {
+        "type": "object",
+        "properties": {
+          "source": {
+            "description": "Current local path for the file",
+            "type": "string"
+          },
+          "target": {
+            "description": "Desired remote path for the file",
+            "type": "string"
+          },
+          "mode": {
+            "description": "Desired access mode for the file after uploading",
+            "type": "integer"
+          },
+          "execute": {
+            "description": "Whether the file should be executed after uploading",
+            "type": "boolean"
+          }
+        },
+        "required": ["source", "target", "mode", "execute"]
+      },
+      "uniqueItems": true
+    }
+  },
+  "required": ["api-key", "api-secret", "server", "files"]
+}
\ No newline at end of file