# Deploy a Workload from a public image
This guide takes you from nothing to a running Workload using only the API. It runs `nginx` from Docker Hub, but any public image works the same way.

The Workload was previously called a Container; the guide file names keep the old word.

## Before you start

You need an account with an organization, a wallet balance or another way to pay, and a token. The examples read two environment variables:

```bash
export ZAROZ_API="https://kernel.zaroz.cloud/api/v2"
export ZAROZ_TOKEN="<your session token or personal access token>"
```

Every request sends `Authorization: Bearer $ZAROZ_TOKEN`. The TypeScript examples need Node 20 or later (run with `npx tsx file.ts`); the Python examples need `pip install requests`. All of them use this small helper, shown once:

**TypeScript**

```ts
const API = process.env.ZAROZ_API ?? "https://kernel.zaroz.cloud/api/v2";
const TOKEN = process.env.ZAROZ_TOKEN!;

export async function api(method: string, path: string, body?: unknown) {
  const res = await fetch(API + path, {
    method,
    headers: { Authorization: `Bearer ${TOKEN}`, "Content-Type": "application/json" },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  if (!res.ok) throw new Error(`${method} ${path} -> ${res.status} ${await res.text()}`);
  return res.status === 204 ? null : res.json();
}
```

**Python**

```python
import os, requests

API = os.environ.get("ZAROZ_API", "https://kernel.zaroz.cloud/api/v2")
S = requests.Session()
S.headers["Authorization"] = "Bearer " + os.environ["ZAROZ_TOKEN"]

def api(method, path, body=None, **params):
    r = S.request(method, API + path, json=body, params=params or None)
    r.raise_for_status()
    return None if r.status_code == 204 else r.json()
```

## 1. Find the product, a plan and a region

List the catalog and pick the Workload product by its `name`. Then ask for its plans in a region, and for the regions you can buy in.

```bash
curl -s "$ZAROZ_API/catalog/products?page_size=100" -H "Authorization: Bearer $ZAROZ_TOKEN" | jq '.data[] | {id, name, slug}'
curl -s "$ZAROZ_API/catalog/regions?product_id=$PRODUCT_ID" -H "Authorization: Bearer $ZAROZ_TOKEN" | jq '.data[] | {id, name, available}'
curl -s "$ZAROZ_API/catalog/products/$PRODUCT_ID/plans?region_id=$REGION_ID" -H "Authorization: Bearer $ZAROZ_TOKEN" | jq '.data[] | {id, name, price, available}'
```

```ts
import { api } from "./api";
const products = await api("GET", "/catalog/products?page_size=100");
const product = products.data.find((p: any) => /workload/i.test(p.name));
const regions = await api("GET", `/catalog/regions?product_id=${product.id}`);
const region = regions.data.find((r: any) => r.available);
const plans = await api("GET", `/catalog/products/${product.id}/plans?region_id=${region.id}`);
const plan = plans.data.find((p: any) => p.available);
```

```python
products = api("GET", "/catalog/products", page_size=100)
product = next(p for p in products["data"] if "workload" in p["name"].lower())
regions = api("GET", "/catalog/regions", product_id=product["id"])
region = next(r for r in regions["data"] if r["available"])
plans = api("GET", f"/catalog/products/{product['id']}/plans", region_id=region["id"])
plan = next(p for p in plans["data"] if p["available"])
```

`plan.price` is the monthly price; the extra-unit rate is that divided by 720 (see [Billing](/units/billing)).

## 2. Quote the purchase

A quote prices the purchase without buying anything. Send the checkout body without `name` and `slug`.

```bash
curl -s -X POST "$ZAROZ_API/checkout/quote" \
  -H "Authorization: Bearer $ZAROZ_TOKEN" -H "Content-Type: application/json" \
  -d "{\"product_id\":\"$PRODUCT_ID\",\"plan_id\":$PLAN_ID,\"region_id\":$REGION_ID}"
```

```ts
const quote = await api("POST", "/checkout/quote", {
  product_id: product.id, plan_id: plan.id, region_id: region.id,
});
console.log(quote.total_cents, quote.currency, quote.cadence_label);
```

```python
quote = api("POST", "/checkout/quote", {
    "product_id": product["id"], "plan_id": plan["id"], "region_id": region["id"],
})
print(quote["total_cents"], quote["currency"], quote["cadence_label"])
```

## 3. Buy it

Checkout creates a **pending** subscription and the invoice that establishes it. `name` is yours to choose. `slug` is the URL segment that addresses the service, unique inside its project; it also becomes the Workload's [private name](/units/private-names).

```bash
curl -s -X POST "$ZAROZ_API/checkout" \
  -H "Authorization: Bearer $ZAROZ_TOKEN" -H "Content-Type: application/json" \
  -d "{\"product_id\":\"$PRODUCT_ID\",\"plan_id\":$PLAN_ID,\"region_id\":$REGION_ID,\"name\":\"Web\",\"slug\":\"web\"}"
```

```ts
const order = await api("POST", "/checkout", {
  product_id: product.id, plan_id: plan.id, region_id: region.id,
  name: "Web", slug: "web",
});
const subscriptionId = order.subscription.id;
const invoiceId = order.invoice.id;
```

```python
order = api("POST", "/checkout", {
    "product_id": product["id"], "plan_id": plan["id"], "region_id": region["id"],
    "name": "Web", "slug": "web",
})
subscription_id = order["subscription"]["id"]
invoice_id = order["invoice"]["id"]
```

Add `organization_id` to bill an organization rather than your personal one, and `project_id` to file it in a specific project. If you leave `project_id` out, the organization's default project is used.

## 4. Pay the invoice

From the wallet:

```bash
curl -s -X POST "$ZAROZ_API/billing/invoices/$INVOICE_ID/pay-with-wallet" -H "Authorization: Bearer $ZAROZ_TOKEN"
```

```ts
await api("POST", `/billing/invoices/${invoiceId}/pay-with-wallet`);
```

```python
api("POST", f"/billing/invoices/{invoice_id}/pay-with-wallet")
```

To pay by card or PayPal instead, start the invoice checkout (`/billing/invoices/{id}/checkout/mollie` or `.../paypal`) and follow the returned link. Once paid, the subscription activates and provisioning starts on its own.

## 5. Find the service instance

The subscription is the purchase; the **service instance** is the thing that runs. Read the subscription for its `project_id` and its resources for the instance id. Wait until the status is `active`.

```bash
curl -s "$ZAROZ_API/subscriptions/$SUBSCRIPTION_ID" -H "Authorization: Bearer $ZAROZ_TOKEN" | jq '{status, project_id}'
curl -s "$ZAROZ_API/subscriptions/$SUBSCRIPTION_ID/resources" -H "Authorization: Bearer $ZAROZ_TOKEN" | jq '.service_instance.id'
```

```ts
const sub = await api("GET", `/subscriptions/${subscriptionId}`);   // wait for sub.status === "active"
const projectId = sub.project_id;
const res = await api("GET", `/subscriptions/${subscriptionId}/resources`);
const serviceId = res.service_instance.id;
```

```python
sub = api("GET", f"/subscriptions/{subscription_id}")   # wait for sub["status"] == "active"
project_id = sub["project_id"]
res = api("GET", f"/subscriptions/{subscription_id}/resources")
service_id = res["service_instance"]["id"]
```

If the product asks setup questions, `GET /service-instances/{id}/setup` lists the unanswered steps, and answering the last one starts the first provisioning (`PUT /service-instances/{id}/setup/steps/{step}`).

## 6. Say which image to run

Saving the source does not start anything; it only records what the next deployment pulls. Use `kind: registry`. For a public image, send no `credentials`.

```bash
curl -s -X PUT "$ZAROZ_API/service-instances/$SERVICE_ID/source" \
  -H "Authorization: Bearer $ZAROZ_TOKEN" -H "Content-Type: application/json" \
  -d '{"kind":"registry","host":"docker.io","repository":"nginx","tag":"1.27"}'
```

```ts
const source = await api("PUT", `/service-instances/${serviceId}/source`, {
  kind: "registry", host: "docker.io", repository: "nginx", tag: "1.27",
});
console.log(source.image_reference);   // docker.io/library/nginx:1.27
```

```python
source = api("PUT", f"/service-instances/{service_id}/source", {
    "kind": "registry", "host": "docker.io", "repository": "nginx", "tag": "1.27",
})
print(source["image_reference"])
```

Zaroz qualifies a single-segment Docker Hub name with `library/`, so `nginx` works. Use a specific tag; the platform does not re-pull when a tag moves (`watch_tag` is always false).

## 7. Deploy

A deployment takes no body. It ships the source exactly as saved.

```bash
curl -s -X POST "$ZAROZ_API/service-instances/$SERVICE_ID/deployments" -H "Authorization: Bearer $ZAROZ_TOKEN"
```

```ts
const deployment = await api("POST", `/service-instances/${serviceId}/deployments`);
// poll until the outcome is recorded
for (;;) {
  const d = await api("GET", `/service-instances/${serviceId}/deployments/${deployment.id}`);
  if (d.outcome) { console.log(d.outcome, d.failure_reason); break; }
  await new Promise((r) => setTimeout(r, 3000));
}
```

```python
import time
deployment = api("POST", f"/service-instances/{service_id}/deployments")
while True:
    d = api("GET", f"/service-instances/{service_id}/deployments/{deployment['id']}")
    if d["outcome"]:
        print(d["outcome"], d.get("failure_reason")); break
    time.sleep(3)
```

`outcome` is `succeeded` or `failed`; `null` means not recorded yet, never a failure. Your Workload now runs one unit.

## Next

- [Add a public port](/guides/containers-public-port) so the internet can reach it.
- [Scale it](/guides/containers-scale) to more than one unit.
- [Read its logs](/guides/containers-logs).
