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

# Web Integration Tutorial

> An optional exercise using a shared HTML template to display a banner, send clicks, and verify impression and click metrics.

This **optional exercise** displays the banner created in [Ad Serving Quickstart](https://docs.aiderx.io/development/en/quick-start.md) on a web page. Enter your A2 server URL and AdUnit ID in the HTML template, load the ad, send a click, then verify metrics in Ad Manager.

## 1. Prepare the example template

Use the same environment and AdUnit ID that returned an HTTP `200` ad response in the Quickstart. Prepare a web browser and Python 3. Ad requests and click delivery do not require an access token.

Create a folder for the example file.

```bash theme={null}
mkdir -p ~/a2-banner-example
cd ~/a2-banner-example
```

Copy the complete HTML below and save it as **plain text** at `~/a2-banner-example/banner-ad.html` using your text editor.

> [accordion] View and copy the HTML template
>
> ```html banner-ad.html theme={null}
> <!doctype html>
> <html lang="en">
>   <head>
>     <meta charset="utf-8" />
>     <meta name="viewport" content="width=device-width, initial-scale=1" />
>     <title>A2 Banner Example</title>
>     <style>
>       * { box-sizing: border-box; }
>       body { margin: 0; background: #f6f8fa; color: #172b4d; font: 16px/1.6 system-ui, sans-serif; }
>       main { max-width: 820px; margin: 40px auto; padding: 28px; background: white; border: 1px solid #dce3eb; border-radius: 12px; }
>       h1 { margin: 0 0 8px; font-size: 28px; }
>       p { margin: 8px 0 20px; }
>       label { display: block; margin-top: 16px; font-weight: 600; }
>       input { display: block; width: 100%; margin-top: 6px; padding: 10px; font: inherit; border: 1px solid #a9b5c6; border-radius: 6px; }
>       button { margin-top: 20px; padding: 10px 22px; border: 0; border-radius: 6px; background: #175cd3; color: white; font: inherit; cursor: pointer; }
>       button:disabled { opacity: .6; cursor: wait; }
>       .hint { color: #52627a; font-size: 14px; }
>       #status { margin: 24px 0; padding: 16px; background: #f1f5fa; border-radius: 6px; }
>       #status p { margin: 0; overflow-wrap: anywhere; }
>       #slot img { display: block; max-width: 100%; height: auto; }
>       #slot a { display: inline-block; position: relative; }
>       .badge { position: absolute; top: 6px; right: 6px; padding: 0 5px; border-radius: 3px; background: #fff; color: #172b4d; font-size: 11px; }
>       details { margin-top: 24px; }
>       summary { cursor: pointer; font-weight: 600; }
>       pre { max-height: 380px; overflow: auto; padding: 16px; background: #10243a; color: #e8eef5; font-size: 13px; }
>       @media (max-width: 600px) { main { margin: 16px; padding: 20px; } }
>     </style>
>   </head>
>   <body>
>     <main>
>       <h1>A2 Banner Example</h1>
>       <p>Enter your A2 server URL and AdUnit ID to display a 300×250 banner.</p>
>       <form id="settings">
>         <label for="base-url">A2 server URL</label>
>         <input id="base-url" type="url" placeholder="&lt;A2_API_URL&gt;" required />
>         <label for="tagid">AdUnit ID</label>
>         <input id="tagid" placeholder="Paste the AdUnit ID from Ad Manager" required pattern="[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}" />
>         <button id="load" type="submit">Load ad</button>
>       </form>
>       <p class="hint">Use On Ad Response tracking. Each successful ad response records an impression. Click the banner once to send a click event.</p>
>       <section id="status" aria-live="polite" aria-label="Request status">
>         <p id="request">Ad request: waiting</p>
>         <p id="display">Rendering: waiting</p>
>         <p id="event">Click delivery: waiting</p>
>       </section>
>       <div id="slot"></div>
>       <p class="hint">Verify collected impressions and clicks in Ad Manager's AdUnit Analytics.</p>
>       <details>
>         <summary>Response JSON</summary>
>         <pre id="response">No response yet.</pre>
>       </details>
>     </main>
>     <script>
>       const form = document.querySelector("#settings");
>       const load = document.querySelector("#load");
>       const requestStatus = document.querySelector("#request");
>       const displayStatus = document.querySelector("#display");
>       const eventStatus = document.querySelector("#event");
>       const slot = document.querySelector("#slot");
>       const responseText = document.querySelector("#response");
>
>       function httpUrl(value) {
>         const url = new URL(value);
>         if (!["http:", "https:"].includes(url.protocol)) {
>           throw new Error("Use an HTTP or HTTPS URL");
>         }
>         return url;
>       }
>
>       async function sendClick(baseUrl, adLog) {
>         eventStatus.textContent = "Click delivery: sending";
>         try {
>           const result = await fetch(`${baseUrl}/dmp/v0/log`, {
>             method: "POST",
>             headers: { "Content-Type": "application/json" },
>             body: JSON.stringify({ type: "ad_log", action: "clk", ad_log: adLog }),
>             keepalive: true,
>           });
>           if (!result.ok) throw new Error(`HTTP ${result.status}`);
>           eventStatus.textContent = `Click delivery: HTTP ${result.status} — check metrics`;
>         } catch (error) {
>           eventStatus.textContent = `Click delivery failed: ${error.message}. Check Network and Console before retrying.`;
>         }
>       }
>
>       form.addEventListener("submit", async (event) => {
>         event.preventDefault();
>         load.disabled = true;
>         requestStatus.textContent = "Ad request: loading";
>         displayStatus.textContent = "Rendering: waiting";
>         eventStatus.textContent = "Click delivery: waiting";
>         slot.replaceChildren();
>         responseText.textContent = "Waiting for response...";
>         try {
>           const baseUrl = httpUrl(document.querySelector("#base-url").value.trim()).href.replace(/\/+$/, "");
>           const tagid = document.querySelector("#tagid").value.trim();
>           const result = await fetch(`${baseUrl}/app/v0/ad/${encodeURIComponent(tagid)}`, {
>             method: "POST",
>             headers: { "Content-Type": "application/json" },
>             body: JSON.stringify({ user: { id: "quickstart-user-001" } }),
>           });
>           const text = await result.text();
>           responseText.textContent = text || "(Empty response body)";
>           if (result.status === 204) throw new Error("HTTP 204: no eligible ad. Check the AdUnit, allocation, and line item period.");
>           if (!result.ok) throw new Error(`HTTP ${result.status}`);
>           const response = JSON.parse(text);
>           responseText.textContent = JSON.stringify(response, null, 2);
>           const banner = response.ad?.display?.banner;
>           const adLog = response.ext?.ad_log;
>           if (!banner?.img || !banner.link?.url || !adLog) {
>             throw new Error("Missing banner image, landing URL, or ad_log. Use the Quickstart image banner.");
>           }
>           const imageUrl = httpUrl(banner.img);
>           const landingUrl = httpUrl(banner.link.url);
>           requestStatus.textContent = `Ad request: HTTP ${result.status}`;
>           const image = document.createElement("img");
>           image.width = response.ad.display.w || 300;
>           image.height = response.ad.display.h || 250;
>           image.alt = "A2 Quickstart sample ad";
>           image.onload = () => { displayStatus.textContent = "Rendering: complete"; };
>           image.onerror = () => { displayStatus.textContent = "Rendering: image failed to load. Check the image URL in Response JSON."; };
>           const link = document.createElement("a");
>           link.href = landingUrl.href;
>           link.target = "_blank";
>           link.rel = "noopener noreferrer";
>           link.addEventListener("click", () => { void sendClick(baseUrl, adLog); });
>           link.append(image);
>           if (banner.ext?.show_ad_badge) {
>             const badge = document.createElement("span");
>             badge.className = "badge";
>             badge.textContent = "Ad";
>             link.append(badge);
>           }
>           slot.replaceChildren(link);
>           image.src = imageUrl.href;
>         } catch (error) {
>           requestStatus.textContent = `Ad request failed: ${error.message}`;
>           if (error instanceof TypeError) {
>             requestStatus.textContent += ". Check the server URL, HTTPS, and CORS in the browser's Network and Console tabs.";
>           }
>         } finally {
>           load.disabled = false;
>         }
>       });
>     </script>
>   </body>
> </html>
> ```

Run the following from the `~/a2-banner-example` folder containing the file. Port `9000` avoids a conflict with the A2 server.

```bash theme={null}
python3 -m http.server 9000 --bind 127.0.0.1
```

Open [http://localhost:9000/banner-ad.html](http://localhost:9000/banner-ad.html).

## 2. Request and display the ad

Fill in the template's fields and select **Load ad** once. You do not need to edit the HTML source.

| Field | Value |
| - | - |
| A2 server URL | Enter the API base URL used for `<A2_API_URL>` in the Quickstart. |
| AdUnit ID | The ID of `Quickstart banner`. Do not use its code or a line item ID. |

Confirm `Ad request: HTTP 200`, `Rendering: complete`, and the A2 banner. Expand **Response JSON** to inspect the complete response for this request.

This page requests a new ad. It stores the `ext.ad_log` from **the ad response displayed on the page** and sends it automatically when the banner is clicked. For HTTP `204`, follow the [Ad Serving Quickstart checklist](https://docs.aiderx.io/development/en/quick-start.md#if-no-ad-is-returned).

> [!NOTE]
>
> This example uses the **On Ad Response** impression tracking configured in the Quickstart. Receiving a banner response records one more impression. Do not send an additional `imp` event.

## 3. Send the click event

Click the displayed banner **once**. Confirm that `example.com` opens in a new tab and the exercise page shows `Click delivery: HTTP 200 — check metrics`.

![Web example showing the server URL, AdUnit ID, HTTP 200 response, and rendered banner](https://mintcdn.com/aiderx/4YOyAVOLclNxMCjG/images/development/web-example.jpg?fit=max\&auto=format\&n=4YOyAVOLclNxMCjG\&q=85\&s=fe978d26bc27d86a9ed95e7ab8ef91b0)

The shared template after entering the server URL and AdUnit ID and verifying banner rendering and click delivery.

The example's click handler sends this body to `POST /dmp/v0/log` with that ad's `ad_log`. The page has already sent it; do not send it again from the terminal.

```json Body sent on click theme={null}
{
  "type": "ad_log",
  "action": "clk",
  "ad_log": "<ext.ad_log from the displayed ad>"
}
```

See [Ad Event Tracking](https://docs.aiderx.io/development/en/ad-events-and-logs.md) for sending clicks from your service. HTTP `200` does not guarantee collection or aggregation, so continue through the metrics check.

## 4. Verify recorded impressions and clicks

**1. Open AdUnit Analytics**

In Ad Manager, go to **Analytics > Inventory Analytics > AdUnit Analytics**.

**2. Select the exercise AdUnit and period**

Search using the copied AdUnit ID to find `Quickstart banner`. Include the exercise date in the query period. Align dates to `Asia/Seoul` when comparing with the console.

**3. Compare with expected counts**

Compare impressions and clicks with the table below. Aggregation can be delayed, so reload the page later. Do not request another ad or resend clicks merely to check metrics.

![Date range and impression and click counts in the AdUnit Analytics detail view](https://mintcdn.com/aiderx/4YOyAVOLclNxMCjG/images/development/en/ad-unit-analytics-detail.png?fit=max\&auto=format\&n=4YOyAVOLclNxMCjG\&q=85\&s=d36c68411edfddb54e6c8a496824ce45)

The Basic Analysis view of an ad unit in AdUnit Analytics. Check the date range and impression and click counts. Values shown are examples.

If the new AdUnit received only this exercise's requests, expect the following counts.

| Action | Impressions | Clicks |
| - | - | - |
| Receive one banner response in the terminal. | `+1`. | No increase. |
| Receive one banner response on the web page. | `+1`. | No increase. |
| Click the page's banner once. | No increase. | `+1`. |
| Total | Confirm `2`. | Confirm `1`. |

If you made extra requests or reloaded the page and loaded another ad, add each successful banner response to the expected impression count. If other traffic reached the AdUnit, compare all request records for the same AdUnit and period.

### Completion criteria

* The banner response contains an image, landing URL, and `ext.ad_log`.
* The page displays the banner, and clicking opens the landing page.
* You have checked the click request and HTTP response.
* **You have verified actual impression and click aggregates in AdUnit Analytics.**

Aggregation timing can vary with the A2 environment's configuration. After the interval provided by your deployment contact has passed, reload the analytics page. If you do not know the expected interval, ask your contact; do not repeat ad requests or clicks just to check metrics.

If the expected aggregates still do not appear after querying the same AdUnit, date, and timezone again, the exercise is incomplete. Prepare request times and timezone, AdUnit ID, responses, click delivery records, and query scope, then follow the support procedure in [Verification and Troubleshooting](https://docs.aiderx.io/development/en/integration-verification-and-troubleshooting.md). Individual event collection status cannot currently be queried.

## Troubleshooting

> [accordion] Requests succeed in the terminal but fail in the browser
>
> Check the server URL, then inspect HTTPS, CORS, and preflight errors in the browser's Network and Console tabs. The API must allow requests from `http://localhost:9000`; duplicate `Access-Control-Allow-Origin` headers cause the browser to block the response.

> [accordion] The banner image does not appear
>
> Check image URL accessibility and blocked image requests. An impression may already be recorded when the ad response is generated.

> [accordion] Click counts do not increase
>
> Check click delivery results, the query period, and AdUnit ID. Network errors or timeouts do not prove processing failure; do not retry unconditionally.

> [accordion] Impressions or clicks exceed expectations
>
> Check repeated requests, loading another ad after a reload, multiple clicks, or other traffic.

## Next steps

See [Ad Event Tracking](https://docs.aiderx.io/development/en/ad-events-and-logs.md) to record impressions, clicks, and conversions in your service.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.