> ## Documentation Index
> Fetch the complete documentation index at: https://docs.adclear.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Link submission

> Submit a URL or a Figma frame instead of uploading a file. Adclear captures it as an image you reference when creating a promotion.

Not everything you need reviewed exists as a file. A landing page, a live promotion, or a design still in Figma can be submitted as a link — Adclear captures it and hands back an `uploadId`, which you use exactly like one from the [File API](/developer/file-upload).

<Note>
  The captured artefact is an image, so create the promotion with `fileFormat: "image"` and the returned upload ID in `uploadIds`.
</Note>

## Which links are supported

| Link               | How it is captured             | Response                  |
| ------------------ | ------------------------------ | ------------------------- |
| Any `http(s)` page | Full-page screenshot           | `201` — ready immediately |
| A Figma frame      | Exported through the Figma API | `202` — poll until ready  |

Figma files must be shared with **anyone with the link**. Adclear cannot read private Figma files submitted through the API.

## Capturing a web page

A capture runs while you wait and can take up to two minutes, so set a generous client timeout.

**Request:**

```http theme={null}
POST https://public-api.adclear.ai/v1/links

{
  "url": "https://example.com/summer-campaign"
}
```

**Response:**

```json theme={null}
{
  "uploadId": "a1b2c3d4-...",
  "status": "completed",
  "fileFormat": "image",
  "sourceUrl": "https://example.com/summer-campaign",
  "originalFileName": "screenshot-example.com.png",
  "previewUrl": "https://storage.googleapis.com/...",
  "correlationId": "..."
}
```

`previewUrl` is a time-limited signed link for previewing the capture. Don't store it — it expires. The `uploadId` does not.

<CodeGroup>
  ```bash curl theme={null}
  curl -s -X POST "$API_BASE/v1/links" \
    -H "Authorization: Bearer $ADCLEAR_API_KEY" \
    -H "X-External-User-ID: $USER_ID" \
    -H "Content-Type: application/json" \
    -d '{ "url": "https://example.com/summer-campaign" }'
  ```

  ```typescript TypeScript theme={null}
  const res = await fetch(`${API_BASE}/v1/links`, {
    method: "POST",
    headers,
    body: JSON.stringify({ url: "https://example.com/summer-campaign" }),
    signal: AbortSignal.timeout(180_000),
  });
  if (!res.ok) throw new Error(`Capture failed (${res.status})`);
  const { uploadId } = await res.json();
  ```

  ```python Python theme={null}
  res = requests.post(
      f"{API_BASE}/v1/links",
      headers=headers,
      json={"url": "https://example.com/summer-campaign"},
      timeout=180,
  )
  res.raise_for_status()
  upload_id = res.json()["uploadId"]
  ```
</CodeGroup>

## Capturing a Figma frame

A Figma link identifies a *file*; Adclear needs to know which **frame** to import. Provide it either way:

* Paste a frame link — in Figma, right-click a frame and choose **Copy link to selection**. The URL carries a `node-id`, which Adclear reads automatically.
* Or send the file URL plus an explicit `figmaFrameId`, discovered with [frame lookup](#discovering-frames).

Sending a file-level URL with no frame returns `400`, pointing you at the frame lookup endpoint.

<Steps>
  <Step title="Start the import">
    **Request:**

    ```http theme={null}
    POST https://public-api.adclear.ai/v1/links

    {
      "url": "https://www.figma.com/design/AbC123/Campaign?node-id=1-2"
    }
    ```

    **Response:**

    ```json theme={null}
    {
      "uploadId": "e5f6a7b8-...",
      "status": "processing",
      "fileFormat": "image",
      "sourceUrl": "https://www.figma.com/design/AbC123/Campaign?node-id=1-2",
      "frameId": "1:2",
      "frameName": "Hero Banner",
      "statusUrl": "/v1/links/e5f6a7b8-...",
      "correlationId": "..."
    }
    ```

    Unlike a web page, a Figma import runs in the background and returns `202`.
  </Step>

  <Step title="Poll until it is ready">
    ```http theme={null}
    GET https://public-api.adclear.ai/v1/links/{uploadId}
    ```

    ```json theme={null}
    {
      "uploadId": "e5f6a7b8-...",
      "status": "completed",
      "finalUploadId": "c3d4e5f6-...",
      "correlationId": "..."
    }
    ```

    Poll every couple of seconds while `status` is `processing`. When it reaches `completed`, use **`finalUploadId`** when creating the promotion — it can differ from the ID you polled. On `failed`, `errorMessage` explains why.
  </Step>
</Steps>

### Discovering frames

To let someone choose a frame, list what the file contains:

**Request:**

```http theme={null}
POST https://public-api.adclear.ai/v1/links/figma/frames

{
  "url": "https://www.figma.com/design/AbC123/Campaign"
}
```

**Response:**

```json theme={null}
{
  "fileName": "Campaign",
  "pages": [
    {
      "id": "0:1",
      "name": "Page 1",
      "frames": [
        { "frameId": "1:2", "name": "Hero Banner", "width": 1200, "height": 628 }
      ]
    }
  ],
  "correlationId": "..."
}
```

Pass any `frameId` back as `figmaFrameId` when submitting the link.

## Creating a promotion from a captured link

Identical to a file upload — the capture is just another `uploadId`:

```http theme={null}
POST https://public-api.adclear.ai/v1/promotions

{
  "uploadIds": ["a1b2c3d4-..."],
  "fileFormat": "image",
  "promotionName": "Summer campaign landing page",
  "channelIds": ["..."],
  "productIds": ["..."],
  "jurisdictionId": "..."
}
```

From here, [trigger an evaluation](/developer/evaluation) as usual.

<Note>
  The submitted URL is not stored on the promotion. To keep it alongside the review, pass it in [`metadata`](/developer/promotions) — it is echoed back in webhook payloads.
</Note>

## Rate limits

Captures call third-party services, so they are limited more tightly than the rest of the API: **10 requests per minute per API key** across `POST /v1/links` and `POST /v1/links/figma/frames`. Polling `GET /v1/links/{uploadId}` is not affected and falls under the standard 60/minute limit.

A `429` includes a `Retry-After` header. See [Errors & rate limits](/developer/errors).

## Common error scenarios

| Scenario                                      | HTTP status | Error code         | What to do                                                                                         |
| --------------------------------------------- | ----------- | ------------------ | -------------------------------------------------------------------------------------------------- |
| Page not found, or the domain doesn't resolve | 400         | `INVALID_REQUEST`  | The message carries the reason from the capture service — check the URL                            |
| Site blocks automated access                  | 400         | `INVALID_REQUEST`  | The page can't be captured; upload a screenshot via the [File API](/developer/file-upload) instead |
| Non-`http(s)` URL                             | 400         | `VALIDATION_ERROR` | Only `http` and `https` are supported                                                              |
| Figma link with no frame                      | 400         | `VALIDATION_ERROR` | Use a "copy link to selection" URL, or supply `figmaFrameId`                                       |
| Figma file is private                         | 403         | `FORBIDDEN`        | Share it with "anyone with the link"                                                               |
| Figma file or frame missing                   | 404         | `NOT_FOUND`        | Check the URL and that the frame still exists                                                      |
| Capture took too long                         | 504         | `UPSTREAM_ERROR`   | Retry; if it persists the page is likely too heavy to capture                                      |

For cross-cutting errors (401, 403, 429, 502), see [Errors & rate limits](/developer/errors).

## Frequently asked questions

<AccordionGroup>
  <Accordion title="Is the page captured again when I create the promotion?" icon="circle-question">
    No. The capture happens once, when you call `POST /v1/links`. The promotion references that stored image, so a later change to the page does not alter what was reviewed.
  </Accordion>

  <Accordion title="Can I reuse one capture across several promotions?" icon="circle-question">
    Yes. The returned upload ID behaves like any other — reference it from as many promotions or versions as you like.
  </Accordion>

  <Accordion title="Why is a screenshot immediate but Figma is not?" icon="circle-question">
    A screenshot is captured inline while the request is open. A Figma frame is exported through the Figma API and processed in the background, so it returns `202` with a `statusUrl` to poll.
  </Accordion>

  <Accordion title="My Figma frame fails as too large. Why?" icon="circle-question">
    Very large frames (over roughly 6 megapixels) need a conversion step that isn't available through the API yet. Export the frame as an image and upload it via the [File API](/developer/file-upload), or import it through the Adclear app.
  </Accordion>

  <Accordion title="Can I submit a PDF by URL?" icon="circle-question">
    A link to a PDF is captured as a screenshot of the rendered page, not downloaded as a PDF. To have the PDF itself reviewed, fetch it yourself and upload it via the [File API](/developer/file-upload).
  </Accordion>
</AccordionGroup>
