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

# Goal conversion pixel: mint the sig, embed the <img>, verify with curl

> Runnable recipes for the `pixel_fire` goal's two root-mounted public ops — mint the HMAC sig for a goal+contact pair, embed the pixel `<img>` on a checkout/signup confirmation, probe `GET /g/{goalId}/{contactId}.gif` and `GET /l/{code}` with curl, then close the conversion loop and avoid replay/abuse.

# Goal conversion pixel recipes

A `pixel_fire` goal fires when a third-party page (your checkout "thank you", your signup confirmation, your partner's success screen) renders a signed `<img src>`. This page walks the full cycle on the wire — mint the signature, embed the pixel, probe it with curl, close the conversion loop, then covers the short-link redirect (`GET /l/{code}`) in the same frame. The [public pixels, redirects, and short links](/concepts/public-pixels-redirects) concept page is the contract; the [Goals API reference](/api-reference/analytics-goals) is the per-endpoint shape.

Nothing here ships an API key into a public URL. The `<img>` hits an anonymous, root-mounted endpoint and authenticates purely with the HMAC in the query string.

## Task index

| # | Recipe | Ops used |
| - | - | - |
| 1 | [Create a `pixel_fire` goal](#1-create-a-pixel_fire-goal) | `POST /api/v1/analytics/goals` |
| 2 | [Mint the HMAC sig for a goal+contact pair](#2-mint-the-hmac-sig) | server-side / hash helper |
| 3 | [Embed the pixel on a checkout/signup page](#3-embed-the-pixel) | `<img>` in HTML |
| 4 | [Probe the pixel and close the loop with curl](#4-probe-the-pixel) | `GET /g/{goalId}/{contactId}.gif`, `GET /api/v1/analytics/goals/:id/conversions` |
| 5 | [Short-link redirect probe](#5-short-link-redirect-probe) | `GET /l/{code}` |
| 6 | [Replay and abuse notes](#6-replay-and-abuse-notes) | — |
| 7 | [Sandbox flavor](#7-sandbox-flavor) | — |

## 1. Create a `pixel_fire` goal

```bash cURL theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/analytics/goals \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Checkout complete",
    "type": "pixel_fire"
  }'
```

The response carries the goal's embed hint. `pixel_url` is the template you fill in:

```json theme={null}
{
  "data": {
    "id": "goal_7XQ2W",
    "name": "Checkout complete",
    "type": "pixel_fire",
    "pixel_url": "https://api.orbit.devotel.io/g/goal_7XQ2W/{contact_id}.gif?sig=<hmac(goalId:contact_id:tenant_schema)>&schema=<tenant_schema>"
  },
  "meta": { "request_id": "req_01HZQX4E7JQ4M2E2H2DM9XE5FJ", "timestamp": "2026-10-03T12:00:00.000Z" }
}
```

Keep `data.id` — every sig below signs it. The **Goals** surface in the dashboard shows the same template beside the goal, so an operator can copy it without reading the API.

## 2. Mint the HMAC sig

The pixel URL's `sig` is a hex HMAC-SHA256 over the pipe-joined tuple `goalId:contactId:tenantSchema`. The tenant schema is part of the signed payload — a leaked URL can't be replayed under a different schema by swapping the `?schema=` param.

Mint server-side (never in a browser bundle — the signing key is your `DEVOTEL_API_SECRET_KEY`, the same key the API client secrets derive from):

```typescript theme={null}
import { createHmac } from "node:crypto";

const sig = createHmac("sha256", DEVOTEL_API_SECRET_KEY)
  .update(`${goalId}:${contactId}:tenant_ac4f2`)
  .digest("hex");
```

For each checkout render, that line produces the query-string value to splice into the `pixel_url` template. The [Node SDK](/sdks/node) and the dashboard's **Goals → embed** panel wrap the same call — the hash helper reads simpler here than a one-line import.

## 3. Embed the pixel

On the checkout or signup "thank you" page template — the page a merchant already renders after the order posts — drop the tag with the minted sig spliced in:

```html theme={null}
<img width="1" height="1"
     src="https://api.orbit.devotel.io/g/goal_7XQ2W/contact_9fMRA.gif?sig=9f0e2d…&schema=tenant_ac4f2"
     alt="" />
```

The 1×1 transparent GIF answers `200` on every render — a forged or malformed sig doesn't break the page and doesn't render a broken-image glyph on the buyer's screen. It just doesn't record.

## 4. Probe the pixel and close the loop

Two curl steps — verify the GIF contract, then read the goal's `/conversions` listing to confirm the row landed:

```bash cURL theme={null}
curl -i "https://api.orbit.devotel.io/g/goal_7XQ2W/contact_9fMRA.gif?sig=9f0e2d…&schema=tenant_ac4f2"
```

```
HTTP/1.1 200 OK
Content-Type: image/gif
Cache-Control: no-store, no-cache, must-revalidate, max-age=0
<43-byte transparent GIF>
```

Close the loop — the conversion record, sourced from the pixel:

```bash cURL theme={null}
curl -s "https://api.orbit.devotel.io/api/v1/analytics/goals/goal_7XQ2W/conversions?limit=5" \
  -H "X-API-Key: dv_test_sk_YOUR_KEY"
```

```json theme={null}
{
  "data": [
    {
      "goal_id": "goal_7XQ2W",
      "contact_id": "contact_9fMRA",
      "conversion_time": "2026-10-03T12:00:04.512Z",
      "value_cents": null,
      "metadata": { "source": "pixel", "ip": "203.0.113.4", "user_agent": "Mozilla/5.0 …", "referer": "https://your-shop.example/checkout/success" }
    }
  ],
  "meta": { "request_id": "req_01HZQX4E7JQ4M2E2H2DM9XE5FJ", "timestamp": "2026-10-03T12:00:06.000Z" }
}
```

`metadata.source: "pixel"` is the tell that the loop came through `GET /g/` and not a webhook/manual call. A forged probe — a sig that doesn't match `goalId:contactId:schema` — still answers `200` with the GIF, but the `/conversions` listing shows nothing for it. That asymmetry is the whole security story.

## 5. Short-link redirect probe

A short link (`POST /api/v1/links` returns one) resolves anonymously through the same root-mounted surface. Probe it the way a browser would — ask for HTML:

```bash cURL theme={null}
curl -i -H "Accept: text/html" "https://api.orbit.devotel.io/l/AbC123"
```

```
HTTP/1.1 302 Found
Location: https://example.com/promo?utm_campaign=may26
```

An unknown code answers with a branded HTML error page when the caller asks for HTML, or a JSON envelope when it asks for JSON. Browsers always send `Accept: text/html` and see the friendly page; your probe sees the envelope:

```
HTTP/1.1 404 Not Found

{ "error": { "code": "LINK_NOT_FOUND", "message": "Unknown or malformed link", "status": 404 } }
```

The short-link family rate-limits tighter than the pixel families (60 req/IP/min vs 120) because a public redirect is trivially pastable.

## 6. Replay and abuse notes

* **The signature binds `goalId + contactId + tenantSchema`.** A replay against a different contact or a swapped `?schema=` fails verification. Leaking a pixel URL exposes exactly one goal+contact conversion — never the class.
* **Per-IP rate limits.** The pixel families ride at 120 requests/IP/minute; `/l/` short links at 60. A loop that fans your own pixel out from one address throttles itself.
* **Idempotent.** Re-rendered pixels don't double-write; the `/conversions` listing collapses to the first hit. First-event-wins — a replayed GET can't corrupt the goal's totals.
* **Fail-closed on any tamper.** No `sig`, no `schema`, malformed schema, mismatched HMAC — all answer `200` + GIF and record nothing. The GIF always comes back.

## 7. Sandbox flavor

Run every step here with `dv_test_sk_…` before you trust the sig-minting code against live. A sandbox key records sandbox conversions on the same wire, so you can prove the full cycle — mint, embed, curl, verify — without touching a real goal. One invalid-sig branch is the check that matters: send a sig of all zeros and confirm the `/conversions` listing stays empty while the GIF still returns.

<Tip>
  For the full pixel/redirect/short-link contract — wire-level response shapes, signing model, and the idempotency model — see [public pixels, redirects, and short links](/concepts/public-pixels-redirects). The [Goals API reference](/api-reference/analytics-goals) has the goal CRUD and conversions listing shapes.
</Tip>


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