# Get started: your first saved buddy

Your server holds the secret key and does everything. The builder in the browser needs only your publishable key, and hands you a look code to save. Three calls, no tokens to mint.

## 1. Get your keys

Sign up at https://buddies.cloud/console/sign-up. You get an organisation, a first project and two test keys:

```sh
BUDDIES_SECRET_KEY=sk_test_…       # server only: can do everything in the project
BUDDIES_PUBLISHABLE_KEY=pk_test_…  # safe in a browser: catalogue, validation, images
```

Test keys see their own data, separate from live. Test mode is free and uncapped.

## 2. The default integration

### Step 1 — your server: fetch the child's buddy

When a child opens the avatar screen, find or create them as a player by your own id, and get their buddies and wardrobe in the same call. The wardrobe says which items they may wear.

```js
// buddies.js: your server only. BUDDIES_SECRET_KEY never leaves it.
const BUDDIES = "https://buddies.cloud";

export async function buddies(method, path, body) {
  const res = await fetch(BUDDIES + path, {
    method,
    headers: {
      Authorization: `Bearer ${process.env.BUDDIES_SECRET_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": crypto.randomUUID(), // makes a retry safe
    },
    body: body && JSON.stringify(body),
  });
  const json = await res.json();
  if (!res.ok) throw new Error(`${json.error.code}: ${json.error.message}`);
  return json;
}

// GET /avatar (your route; the child is signed in to *your* app)
const player = await buddies("POST", "/v1/players", {
  external_id: user.id, // your own opaque id: never an email or name
  include: ["buddies", "wardrobe"],
});
const buddy = player.buddies[0]; // undefined the first time
res.json({ lookCode: buddy?.look_code ?? null, name: buddy?.name ?? null, wardrobe: player.wardrobe });
```

```python
import os, uuid, requests

BUDDIES = "https://buddies.cloud"

def buddies(method, path, body=None):
    r = requests.request(method, BUDDIES + path, json=body, headers={
        "Authorization": f"Bearer {os.environ['BUDDIES_SECRET_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    })
    data = r.json()
    if not r.ok:
        raise RuntimeError(f"{data['error']['code']}: {data['error']['message']}")
    return data

player = buddies("POST", "/v1/players", {"external_id": user.id, "include": ["buddies", "wardrobe"]})
```

`POST /v1/players` returns 201 for a new player and 200 for an existing one, with `buddies` (newest first) and `wardrobe`.

### Step 2 — the browser: show the builder

```html
<div id="avatar"></div>
<script type="module" src="https://buddies.cloud/embed/v1/buddies.js"></script>
<script type="module">
  const me = await fetch("/avatar").then((r) => r.json()); // step 1

  const builder = document.createElement("buddy-builder");
  builder.setAttribute("publishable-key", "pk_test_…");
  builder.setAttribute("save-label", "Save my buddy");
  if (me.lookCode) builder.setAttribute("value", me.lookCode);
  if (me.name) builder.setAttribute("name", me.name);
  builder.wardrobe = me.wardrobe; // locked items show as "Not yet"
  document.getElementById("avatar").append(builder);

  builder.addEventListener("buddy-save", (e) =>
    fetch("/avatar", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(e.detail), // { lookCode, name }
    }),
  );
</script>
```

### Step 3 — your server: save it

Look the buddy up by the signed-in child rather than trusting an id from the browser, then create or update it. The API checks the look against the child's wardrobe, so a tampered page can't wear something locked.

```js
// POST /avatar  body: { lookCode, name }
const { lookCode, name } = req.body;
const { data: [existing] } = await buddies("GET", `/v1/buddies?external_id=${encodeURIComponent(user.id)}&limit=1`);
const saved = existing
  ? await buddies("PATCH", `/v1/buddies/${existing.id}`, { look_code: lookCode, name })
  : await buddies("POST", "/v1/buddies", { external_id: user.id, look_code: lookCode, name });
res.json({ image: saved.image_url });
```

```sh
curl https://buddies.cloud/v1/buddies \
  -H "Authorization: Bearer $BUDDIES_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"external_id":"user_48213","look_code":"1.YnVkZHkvYm9keS5wdWZmLm5pZ2h0L2ZhY2Uuc21pbGUuYmVycnkvb3V0Zml0LnNjYXJmLmVtYmVyL2hlbGQucXVpbGwuZ29sZA","name":"Pip"}'
```

### Step 4 — show it anywhere

Every buddy has a signed `image_url` (no key needed), or draw it live from its look code:

```html
<img src="{buddy.image_url}" width="120" height="120" alt="">
<buddy-avatar look="{buddy.look_code}" size="120" idle></buddy-avatar>
```

That's the whole integration. Rewards, webhooks and GraphQL are optional extras on the same keys.

## 3. Other drop-in modes

1. **Image URL** — `<img src="https://buddies.cloud/v1/render.svg?look=LOOK_CODE&size=160&key=pk_test_…" alt="">`. Nothing stored. `size` 16–1024, `background` a hex colour or name.
2. **Web component** — the default above. With no server at all, keep the look code in your own storage.
3. **Server API** — build your own dress-up screen or make buddies with no builder: `POST /v1/buddies {"external_id":"user_48213","look":{"base":"buddy","slots":{"body":{"item":"puff","color":"night"},"hat":"beanie"}}}`. No look at all gives the base's default look.
4. **Iframe** — `https://buddies.cloud/embed/builder.html#key=pk_test_…&origin=https://your.site`; pass the wardrobe with `postMessage({ source: "buddies", v: 1, type: "set-wardrobe", wardrobe }, "https://buddies.cloud")` after the frame's `ready` message. See embed.md.
5. **GraphQL** — `POST https://buddies.cloud/graphql` with the same keys. See graphql.md.

## 4. When you have no back end

If your product has no server, the browser can talk to Buddies directly with a short-lived player session. It's an advanced option; see authentication.md, "the browser talks to Buddies directly".
