# Onboard a client's product

URL: https://submitator.com/docs/guides/onboard-a-clients-product

> Turn a client's URL into a project that is ready to launch, with what we read from the site, images, a contact email and your own references.

## What you'll build

A draft project for one client: the name, tagline and short description read from their site, a logo, screenshots and a contact email, plus your CRM's id so you can find it later. When `readiness.ready` is `true`, the project is ready to [launch](https://submitator.com/docs/guides/launch-and-track). A draft costs nothing.

## Before you start

- **A key.** A test key works for every step, and test mode accepts sites on `.test`. With a live key, use your client's real site.
- **What your client gives you.** The site's URL, an email address they read, and their logo and 1 to 5 screenshots, as files or URLs.

The contact email matters: some directories register the listing to it and send their emails there. It cannot change once the project launches.

## 1. Create the project from the URL

Only `url` is required. Send the contact email, your own id in `external_id`, and anything else you keep about the client in `metadata`.

```bash
curl https://api.submitator.com/v1/projects \
  -H "Authorization: Bearer $SUBMITATOR_API_KEY" \
  -H "Idempotency-Key: crm_8812-create" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://ledgerly.test", "contact_email": "founder@ledgerly.test", "external_id": "crm_8812", "metadata": {"account_owner": "Priya"}}'
```

```js
const res = await fetch("https://api.submitator.com/v1/projects", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.SUBMITATOR_API_KEY}`,
    "Idempotency-Key": "crm_8812-create",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://ledgerly.test",
    contact_email: "founder@ledgerly.test",
    external_id: "crm_8812",
    metadata: { account_owner: "Priya" },
  }),
});
console.log(await res.json());
```

```python
import os
import requests

res = requests.post(
    "https://api.submitator.com/v1/projects",
    headers={
        "Authorization": f"Bearer {os.environ['SUBMITATOR_API_KEY']}",
        "Idempotency-Key": "crm_8812-create",
    },
    json={
        "url": "https://ledgerly.test",
        "contact_email": "founder@ledgerly.test",
        "external_id": "crm_8812",
        "metadata": {"account_owner": "Priya"},
    },
)
print(res.json())
```

Response 201 (some fields left out):

```json
{
  "object": "project",
  "id": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
  "status": "draft",
  "url": "https://ledgerly.test",
  "name": "Ledgerly",
  "contact_email": "founder@ledgerly.test",
  "logo": null,
  "screenshots": [],
  "autofill": { "status": "running", "name_source": "domain", "filled": [], "image_candidates": [] },
  "readiness": { "ready": false, "missing": ["logo", "screenshots"] },
  "external_id": "crm_8812",
  "metadata": { "account_owner": "Priya" }
}
```

The `Idempotency-Key` makes the call safe to send twice: a repeat within 24 hours returns this same project. Each `url`, `contact_email` and `external_id` belongs to one project per mode, so a second project with any of them returns 409 `project_exists`, with the first one in `existing`.

## 2. Wait for the site to be read

Right after the create call, `autofill.status` is `running` and `name` comes from the domain. Read the project again until the status is `complete`: by then the name, tagline and short description you left empty are filled in.

```bash
curl https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP \
  -H "Authorization: Bearer $SUBMITATOR_API_KEY"
```

```js
let project;
do {
  await new Promise((resolve) => setTimeout(resolve, 5000));
  const res = await fetch("https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP", {
    headers: { Authorization: `Bearer ${process.env.SUBMITATOR_API_KEY}` },
  });
  project = await res.json();
} while (project.autofill.status === "running");
console.log(project.autofill);
```

```python
import os
import time
import requests

while True:
    time.sleep(5)
    project = requests.get(
        "https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
        headers={"Authorization": f"Bearer {os.environ['SUBMITATOR_API_KEY']}"},
    ).json()
    if project["autofill"]["status"] != "running":
        break
print(project["autofill"])
```

autofill once complete:

```json
{
  "status": "complete",
  "name_source": "site",
  "filled": ["name", "tagline", "short_description"],
  "image_candidates": [
    { "url": "https://ledgerly.test/apple-touch-icon.png", "suggested_as": "logo", "width": 180, "height": 180 },
    { "url": "https://ledgerly.test/og-image.png", "suggested_as": "screenshot", "width": 1200, "height": 630 }
  ]
}
```

Autofill fills only the fields you left empty, and never sets an image by itself. When it ends `failed`, the site could not be read: fill in the fields yourself in step 4.

## 3. Add the images

`image_candidates` lists images on the site that could serve: the site's icon as a logo, its social preview as a screenshot. Pass one as `url`, or upload your client's own file as `multipart/form-data`. A project needs 1 logo and 1 to 5 screenshots, in PNG, JPEG, GIF or WebP up to 5 MB.

```bash
curl -X PUT https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP/logo \
  -H "Authorization: Bearer $SUBMITATOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://ledgerly.test/apple-touch-icon.png"}'

curl https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP/screenshots \
  -H "Authorization: Bearer $SUBMITATOR_API_KEY" \
  -F "file=@dashboard.png"
```

```js
import fs from "node:fs";

const base = "https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP";
const auth = { Authorization: `Bearer ${process.env.SUBMITATOR_API_KEY}` };

await fetch(`${base}/logo`, {
  method: "PUT",
  headers: { ...auth, "Content-Type": "application/json" },
  body: JSON.stringify({ url: "https://ledgerly.test/apple-touch-icon.png" }),
});

const form = new FormData();
form.append("file", await fs.openAsBlob("dashboard.png"), "dashboard.png");
const res = await fetch(`${base}/screenshots`, { method: "POST", headers: auth, body: form });
console.log(await res.json());
```

```python
import os
import requests

base = "https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP"
auth = {"Authorization": f"Bearer {os.environ['SUBMITATOR_API_KEY']}"}

requests.put(f"{base}/logo", headers=auth, json={"url": "https://ledgerly.test/apple-touch-icon.png"})

with open("dashboard.png", "rb") as image:
    res = requests.post(f"{base}/screenshots", headers=auth, files={"file": image})
print(res.json())
```

Response 200 (some fields left out):

```json
{
  "object": "project",
  "id": "prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
  "logo": { "object": "image", "id": "img_5Mx2Kq8Wt3Lr9Vb7Nz1HdQ", "status": "ready", "error": null },
  "screenshots": [
    { "object": "image", "id": "img_2Kr9Lx3Wq7Mt5Vb8Nz1HcF", "status": "ready", "error": null }
  ],
  "readiness": { "ready": true, "missing": [] }
}
```

Directories that take a single screenshot use the first one, so add the strongest first. You can also pass `logo_url` and `screenshot_urls` in the create call, and they import in the background.

## 4. Fill in what directories ask for

Directories ask for more than a name: a category, the pricing, tags and the maker. Send what you know with `PATCH`; a field you leave out keeps its value.

```bash
curl -X PATCH https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP \
  -H "Authorization: Bearer $SUBMITATOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"maker_name": "Maya Chen", "category": "saas", "pricing_model": "freemium", "price_amount_cents": 1200, "price_period": "monthly", "tags": ["accounting", "bookkeeping", "invoicing"]}'
```

```js
const res = await fetch("https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.SUBMITATOR_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    maker_name: "Maya Chen",
    category: "saas",
    pricing_model: "freemium",
    price_amount_cents: 1200,
    price_period: "monthly",
    tags: ["accounting", "bookkeeping", "invoicing"],
  }),
});
console.log(await res.json());
```

```python
import os
import requests

res = requests.patch(
    "https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP",
    headers={"Authorization": f"Bearer {os.environ['SUBMITATOR_API_KEY']}"},
    json={
        "maker_name": "Maya Chen",
        "category": "saas",
        "pricing_model": "freemium",
        "price_amount_cents": 1200,
        "price_period": "monthly",
        "tags": ["accounting", "bookkeeping", "invoicing"],
    },
)
print(res.json())
```

| Field | Rule |
| --- | --- |
| `tagline` / `short_description` / `long_description` | Up to 100, 500 and 2,000 characters. Most directories show the short description. |
| `category` | `saas`, `ai_tools`, `dev_tools`, `no_code`, `productivity` or `other`. We map it to each directory's own list. |
| `pricing_model` | `free`, `freemium`, `paid` or `open_source`, with `price_amount_cents` and `price_period` for a paid plan. |
| `tags` | Up to 10, each up to 40 characters. |
| `competitors`, `promo_code`, `audience` | These open more directories: [Reach more directories](https://submitator.com/docs/guides/reach-more-directories). |

`null` or an empty string clears a field. A list you send, such as `tags`, replaces the whole list.

## 5. Check that it is ready

`GET /projects/{project_id}/readiness` is free. It says whether the project can launch, how many directories a launch would get now, and what would unlock more.

```bash
curl https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP/readiness \
  -H "Authorization: Bearer $SUBMITATOR_API_KEY"
```

```js
const res = await fetch(
  "https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP/readiness",
  { headers: { Authorization: `Bearer ${process.env.SUBMITATOR_API_KEY}` } },
);
console.log(await res.json());
```

```python
import os
import requests

res = requests.get(
    "https://api.submitator.com/v1/projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP/readiness",
    headers={"Authorization": f"Bearer {os.environ['SUBMITATOR_API_KEY']}"},
)
print(res.json())
```

Response 200 (some fields left out):

```json
{
  "object": "readiness",
  "ready": true,
  "missing": [],
  "directories": { "selected": 87, "limit": 100, "top_domain_rating": 92, "median_domain_rating": 48 },
  "unlocks": [
    { "requirement": "badge", "directories": 9, "how": "Install the badge on https://ledgerly.test and verify it." }
  ],
  "cost": { "launches": 1, "available": 100 }
}
```

Until `ready` is `true`, `missing` names what to add: `contact_email`, `logo` or `screenshots`.

## Check it worked

- `GET /projects?external_id=crm_8812` returns the project, so your CRM can find it by its own id.
- `readiness.ready` is `true` on the project and on `GET /projects/prj_4QzX1m9Lr2Vb7Nc8Tk3HwP/readiness`.
- `autofill.status` is `complete`, or you filled in the name, tagline and short description yourself.

## What can go wrong

| Code | Status | Fix |
| --- | --- | --- |
| [`project_exists`](https://submitator.com/docs/errors#project_exists) | 409 | Another project has this `url`, `contact_email` or `external_id`. Use the one in `existing`, or change the field. |
| [`validation_failed`](https://submitator.com/docs/errors#validation_failed) | 422 | A field breaks a rule; `errors` names each one. With a live key, a `.test` site fails with `not_public_url`. |
| [`image_unreachable`](https://submitator.com/docs/errors#image_unreachable) | 422 | The image URL did not return an image. Upload the file instead. |
| [`quota_exceeded`](https://submitator.com/docs/errors#quota_exceeded) | 422 | Before your first purchase, test mode allows 25 projects and 100 new ones a day. |

## Next steps

- [Launch and track](https://submitator.com/docs/guides/launch-and-track): spend 1 launch and follow each listing.
- [Reach more directories](https://submitator.com/docs/guides/reach-more-directories): what the unlocks mean and how to get them.
- [Install the badge](https://submitator.com/docs/guides/install-the-badge): the unlock most projects can get.
