# Quickstart

> From a new API key to a published post in six steps.

Canonical: <https://www.tryadeli.com/docs/quickstart>

Six steps: create a key, pick a profile for your customer, connect their
Instagram account to it, and publish an image. Everything here runs against a real account — there is no sandbox
mode, so use an account you do not mind posting to.

You will need an Instagram professional (Business or Creator) account. TikTok
and YouTube follow the same shape; the differences are called out in
[the Posts reference](https://www.tryadeli.com/docs/api/posts).

## 1. Create an API key

Sign in at [`/sign-in`](https://app.tryadeli.com/sign-in) with Google. Any Google account works, and your
first sign-in creates your account, an organization you own, and a default
profile in it.

Open **Settings → API keys** in the dashboard sidebar, or go straight to
[`/settings/api-keys`](https://app.tryadeli.com/settings/api-keys). Give the key a label such as
`Local testing` and select **Create key**.

> **Warning: The secret is shown once**
>
> Copy it immediately — Adeli stores only a hash and cannot show it to you
> again. A key belongs to your organization and can act on every profile in it,
> and deleting a key revokes it instantly.

Save it alongside the base URL:

```bash
export ADELI_URL=https://app.tryadeli.com
export ADELI_API_KEY='rk_live_...'
```

## 2. List your profiles, or create one

A profile is one of your customers. Every request names the profile it acts on,
with a `profileId` or with an `accountId` that belongs to it, and nothing
defaults to one. This call lists your organization's profiles, the default
first, and confirms the key works.

> **Note: Changed on 2026-10-08**
>
> Keys used to be bound to one profile, and requests without a `profileId` used
> it. Keys now belong to the organization, and a request that names no profile
> returns `400 profile_required`. See [Authentication](https://www.tryadeli.com/docs/authentication#breaking-change-2026-10-08).

**cURL**

```bash
curl --fail-with-body "$ADELI_URL/api/v1/profiles" \
  -H "Authorization: Bearer $ADELI_API_KEY"
```

**JavaScript**

```javascript
const adeli = (path, init) =>
  fetch(`${process.env.ADELI_URL}/api/v1${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.ADELI_API_KEY}`,
      "Content-Type": "application/json",
      ...init?.headers,
    },
  }).then((response) => response.json());

const { profiles } = await adeli("/profiles");
const profileId = profiles[0].id;
```

**Python**

```python
import os, requests

BASE = f"{os.environ['ADELI_URL']}/api/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['ADELI_API_KEY']}"}

profiles = requests.get(f"{BASE}/profiles", headers=HEADERS).json()["profiles"]
profile_id = profiles[0]["id"]
```

```json
{
  "profiles": [
    {
      "id": "00000000-0000-4000-8000-000000000001",
      "name": "Client A",
      "externalId": "customer-123",
      "metadata": {},
      "isDefault": true,
      "createdAt": "2026-09-08T20:00:00.000Z",
      "updatedAt": "2026-09-08T20:00:00.000Z"
    }
  ]
}
```

To give a new customer their own profile instead, create one. The optional
`Idempotency-Key` makes a retry return the same profile rather than a
`409 profile_name_conflict`:

```bash
curl --fail-with-body -X POST "$ADELI_URL/api/v1/profiles" \
  -H "Authorization: Bearer $ADELI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: customer-123" \
  -d '{"name":"Client A","externalId":"customer-123"}'
```

Save the profile's `id` — the next three calls need it.

```bash
export PROFILE_ID=00000000-0000-4000-8000-000000000001
```

## 3. Start a connection

This returns an `authUrl`. Open it in **your customer's browser** — they
authorize Instagram directly with Meta, and Adeli never sees their credentials.

**cURL**

```bash
curl --fail-with-body -X POST \
  "$ADELI_URL/api/v1/profiles/$PROFILE_ID/connect" \
  -H "Authorization: Bearer $ADELI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"platform":"instagram","redirectUrl":"https://client.example/callback"}'
```

**JavaScript**

```javascript
const session = await adeli(`/profiles/${profileId}/connect`, {
  method: "POST",
  body: JSON.stringify({
    platform: "instagram",
    redirectUrl: "https://client.example/callback",
  }),
});

// Send the customer here. Do not fetch it from your backend.
console.log(session.authUrl);
```

**Python**

```python
session = requests.post(
    f"{BASE}/profiles/{profile_id}/connect",
    headers=HEADERS,
    json={
        "platform": "instagram",
        "redirectUrl": "https://client.example/callback",
    },
).json()

print(session["authUrl"])  # Send the customer here.
```

```json
{
  "id": "00000000-0000-4000-8000-000000000002",
  "profileId": "00000000-0000-4000-8000-000000000001",
  "platform": "instagram",
  "status": "pending_authorization",
  "accountId": null,
  "expiresAt": "2026-09-08T20:10:00.000Z",
  "completedAt": null,
  "error": null,
  "authUrl": "https://www.instagram.com/oauth/authorize?..."
}
```

> **Note: redirectUrl must be allowlisted**
>
> Its origin has to appear in the server's
> `PUBLIC_API_CONNECT_REDIRECT_ORIGINS` allowlist and use HTTPS, except for
> `localhost` and `127.0.0.1` during development. An origin that is not on the
> list returns `400 invalid_redirect_uri`. You can omit `redirectUrl` entirely
> and just poll instead.

## 4. Poll until the connection lands

The session expires ten minutes after it is created. Poll it until `status` is
one of the three terminal values: `connected`, `failed`, or `expired`.

**cURL**

```bash
curl --fail-with-body \
  "$ADELI_URL/api/v1/profiles/$PROFILE_ID/connect/$CONNECTION_SESSION_ID" \
  -H "Authorization: Bearer $ADELI_API_KEY"
```

**JavaScript**

```javascript
const TERMINAL = new Set(["connected", "failed", "expired"]);

let state = session;
while (!TERMINAL.has(state.status)) {
  await new Promise((resolve) => setTimeout(resolve, 2000));
  state = await adeli(`/profiles/${profileId}/connect/${session.id}`);
}

if (state.status !== "connected") throw new Error(state.error?.code ?? state.status);
const accountId = state.accountId;
```

**Python**

```python
import time

TERMINAL = {"connected", "failed", "expired"}

state = session
while state["status"] not in TERMINAL:
    time.sleep(2)
    state = requests.get(
        f"{BASE}/profiles/{profile_id}/connect/{session['id']}",
        headers=HEADERS,
    ).json()

assert state["status"] == "connected", state["error"]
account_id = state["accountId"]
```

If you supplied a `redirectUrl`, the browser comes back to it with
`connectionSessionId`, `status`, and — on success — `accountId` in the query
string. Treat those as a hint for your UI and poll the session for the
authoritative answer.

## 5. List the connected accounts

**cURL**

```bash
curl --fail-with-body "$ADELI_URL/api/v1/accounts?profileId=$PROFILE_ID&platform=instagram" \
  -H "Authorization: Bearer $ADELI_API_KEY"
```

**JavaScript**

```javascript
const { accounts } = await adeli(`/accounts?profileId=${profileId}&platform=instagram`);
```

**Python**

```python
accounts = requests.get(
    f"{BASE}/accounts", headers=HEADERS, params={"profileId": profile_id, "platform": "instagram"}
).json()["accounts"]
```

```json
{
  "status": "complete",
  "accounts": [
    {
      "id": "00000000-0000-0000-0000-000000000000",
      "accountId": "00000000-0000-0000-0000-000000000000",
      "profileId": "00000000-0000-4000-8000-000000000001",
      "platform": "instagram",
      "providerId": "17841400000000000",
      "displayName": "@adeli",
      "displayIdentifier": "adeli",
      "connectionStatus": "connected"
    }
  ],
  "errors": []
}
```

Use `accountId` — Adeli's UUID — in every later request. It belongs to exactly
one profile, so a request that passes it needs no `profileId`. `providerId` is
informational.

## 6. Publish an image

Images are sent as base64 and must be JPEG, PNG, or WebP, decoding to no more
than 8 MiB. Adeli normalizes them to JPEG before handing them to Instagram.

**cURL**

```bash
# Build the request body without putting base64 on the command line.
node -e '
const fs = require("node:fs");
fs.writeFileSync("/tmp/adeli-post.json", JSON.stringify({
  platform: "instagram",
  accountId: process.env.ACCOUNT_ID,
  caption: "Published through Adeli",
  image: { contentType: "image/png", base64: fs.readFileSync("./photo.png").toString("base64") },
}));
'

curl --fail-with-body -X POST "$ADELI_URL/api/v1/posts" \
  -H "Authorization: Bearer $ADELI_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @/tmp/adeli-post.json
```

**JavaScript**

```javascript
import { readFile } from "node:fs/promises";

const post = await adeli("/posts", {
  method: "POST",
  body: JSON.stringify({
    platform: "instagram",
    accountId,
    caption: "Published through Adeli",
    image: {
      contentType: "image/png",
      base64: (await readFile("./photo.png")).toString("base64"),
    },
  }),
});
```

**Python**

```python
import base64, pathlib

post = requests.post(
    f"{BASE}/posts",
    headers=HEADERS,
    json={
        "platform": "instagram",
        "accountId": account_id,
        "caption": "Published through Adeli",
        "image": {
            "contentType": "image/png",
            "base64": base64.b64encode(pathlib.Path("photo.png").read_bytes()).decode(),
        },
    },
).json()
```

A successful publish returns `201` and the normalized post:

```json
{
  "id": "instagram_post_00000000-0000-0000-0000-000000000000_17900000000000000",
  "platform": "instagram",
  "accountId": "00000000-0000-0000-0000-000000000000",
  "profileId": "00000000-0000-4000-8000-000000000001",
  "providerId": "17900000000000000",
  "caption": "Published through Adeli",
  "media": [{ "type": "IMAGE", "url": "https://media.example/staging/key.jpg", "thumbnailUrl": "https://media.example/staging/key.jpg" }],
  "permalink": null,
  "engagement": { "likes": null, "comments": null },
  "publishedAt": "2026-04-01T12:00:00.000Z"
}
```

Pass `images` instead of `image` — an array of 2 to 10 — to publish a carousel.

## Where to go next

- **[Authentication](https://www.tryadeli.com/docs/authentication)** — key scoping and how to keep keys out of the browser.
- **[Core concepts](https://www.tryadeli.com/docs/concepts)** — partial responses, and the limits on every read.
- **[Posts](https://www.tryadeli.com/docs/api/posts)** — TikTok video, photo, and carousel publishing, and YouTube uploads.
- **[Errors](https://www.tryadeli.com/docs/errors)** — every code the API can return.
