# Run it on your provider

In this quickstart you store your Hetzner Cloud API token in Tarsana and create a server from your spec in your own Hetzner Cloud project.

Before you start, finish [Build your first image](/quickstarts/first-image). You need the shell variables from it: `TENANT`, `ACCESS_TOKEN` and `SUBMISSION`. Creating and removing servers needs the `deployer` role, which the account you signed up with has.

## 1. Create an API token at Hetzner

In the Hetzner Cloud Console, open the project the server should run in, go to **Security**, then **API tokens**, and create a token with **Read & Write** permission. Copy it.

```sh
export HCLOUD_TOKEN=paste-your-hetzner-api-token
```

## 2. Store it in Tarsana

Send the token to [`set-provider-credential`](/api/set-provider-credential). Tarsana stores it encrypted for your tenant only and never shows it again.

```sh
curl -X POST "https://api.tarsana.io/v1/tenants/$TENANT/providers/hetzner/credential" \
  -H "X-Tarsana-Access-Token: $ACCESS_TOKEN" \
  -H "X-Tarsana-Tenant: $TENANT" \
  -H "Content-Type: application/json" \
  --data "{\"provider_token\": \"$HCLOUD_TOKEN\"}"
```

If it worked, you see its fingerprint:

```json
{
  "credential_set": true,
  "fingerprint": "sha256:15dedf1c99544c41",
  "set_at": "2026-10-05T02:15:40.551914Z",
  "replaced": false
}
```

The fingerprint is `sha256:` followed by the first 16 hex digits of the SHA-256 of your token. Check it against your own copy:

```sh
printf %s "$HCLOUD_TOKEN" | sha256sum | cut -c1-16
```

## 3. Check where the server will run

The `placement` in your spec says where: `provider` is `hetzner-cloud`, `server_type` is a Hetzner server type, and `region` is optional. The spec from the first quickstart already has one:

```json
"placement": {"provider": "hetzner-cloud", "server_type": "cpx12", "region": "nbg1"}
```

The placement is not part of the spec's hash, and Tarsana keeps the placement of the first submission of a spec. If you submitted a spec without a placement, adding one does not change the stored submission: change something else in the spec as well, so that it gets a new submission ID, and submit it. A placement Tarsana cannot use is refused at submission with [`spec_invalid`](/api/errors#spec_invalid), together with the providers you can use.

## 4. Create the server

Call [`create-server`](/api/create-server) with a name of your own for this server in `request_id`:

```sh
curl -X POST "https://api.tarsana.io/v1/tenants/$TENANT/submissions/$SUBMISSION/create-server" \
  -H "X-Tarsana-Access-Token: $ACCESS_TOKEN" \
  -H "X-Tarsana-Tenant: $TENANT" \
  -H "Content-Type: application/json" \
  --data '{"request_id": "web-1"}'
```

If it worked, you see the server's ID at Hetzner in `created.target.id`:

```json
{
  "provider_name": "hetzner",
  "created": {
    "action_id": "ff97f81e3825703462ecab41c1c4ffa3",
    "idempotent_replay": false,
    "operation_id": "5000",
    "state": "provisioning",
    "target": {
      "id": "1000",
      "region": "fsn1"
    }
  },
  "first_boot": {
    "carrier": "user-data",
    "runs": "A sentence about the one-time setup.",
    "secret": "A sentence about the server's own enrollment token."
  }
}
```

> **Save the server ID** (`created.target.id`). You need it to remove the server later.

A 200 answer means the server exists at Hetzner. It then boots, runs its one-time setup and connects to Tarsana. Password login is switched off and no SSH key is installed.

If the request fails part of the way, send the same request again with the same `request_id`: you get the same server back, never a second one. The errors you may see:

| Code | What to do |
|---|---|
| [`create_unavailable`](/api/errors#create_unavailable) | Check your stored token with [`provider-credential`](/api/provider-credential). |
| [`not_creatable`](/api/errors#not_creatable) | The stored spec has no `placement`. See step 3. |
| [`not_provisionable`](/api/errors#not_provisionable) | Wait until the build has succeeded. |
| [`create_refused`](/api/errors#create_refused) | Hetzner refused, for example the server type is not sold in that region. Fix the cause and try again. |

## 5. See the other servers in your account

[`servers`](/api/servers) lists the servers in your Hetzner project that Tarsana did not create, and counts the ones it did:

```sh
curl "https://api.tarsana.io/v1/tenants/$TENANT/providers/hetzner/servers" \
  -H "X-Tarsana-Access-Token: $ACCESS_TOKEN" \
  -H "X-Tarsana-Tenant: $TENANT"
```

```json
{
  "accounted_for": 1,
  "servers": [
    {
      "id": "1001",
      "name": "my-own-box",
      "server_type": "cx22",
      "location": "nbg1",
      "state": "running",
      "created": "2026-10-05T02:15:41+00:00",
      "addresses": [
        "198.51.100.11"
      ],
      "labels": {},
      "reason_code": "no_label",
      "reason": "A sentence that says why this server is listed."
    }
  ]
}
```

Your new server is counted in `accounted_for`. A server you made yourself is listed, with the reason Tarsana does not count it as its own. Tarsana never removes a listed server unless you ask.

## 6. Remove the server when you are done

[`decommission-server`](/api/decommission-server) revokes the server's own credentials, deletes it at Hetzner and waits until it is gone:

```sh
export SERVER_ID=paste-the-created.target.id-value
```

```sh
curl -X POST "https://api.tarsana.io/v1/tenants/$TENANT/providers/hetzner/servers/$SERVER_ID/decommission" \
  -H "X-Tarsana-Access-Token: $ACCESS_TOKEN" \
  -H "X-Tarsana-Tenant: $TENANT"
```

```json
{
  "decommissioned": {
    "state": "destroyed",
    "observed": "not-found",
    "action_id": "ff97f81e3825703462ecab41c1c4ffa3",
    "idempotent_replay": false,
    "requested_at": "2026-10-05T02:15:41.166635+00:00",
    "completed_at": "2026-10-05T02:15:41.169045+00:00",
    "target": {
      "id": "1000",
      "region": "fsn1"
    }
  }
}
```

You cannot undo this. If it stops part of the way, send the same request again to continue.

## Next steps

- [Update a server](/quickstarts/update-a-server): move a server to a new image.
- [Remove your stored token](/api/remove-provider-credential) when you no longer want Tarsana to use it.
