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

# Build your own connector

> Run your own publishing target: poll MerchantOps for approved records and report outcomes over a pull-based API.

MerchantOps ships with built-in connectors (see [VTEX](/publishing/vtex)), but
you can also **build your own**. This is the right approach when the destination
is an internal system — an ERP, a proprietary pricing platform, a custom
storefront extension — that a built-in target can't reach.

A custom connector is a small service **you** run. It never connects to
MerchantOps internals. Instead it talks to a **pull-based HTTP API**:
MerchantOps holds the approved, versioned records; your connector polls for the
ones fanned out to your target, pushes them into your system, and reports each
outcome back. Everything is scoped to your organization by an API key, so a
misconfigured connector can never touch another tenant's data.

## The loop

Every connector, regardless of destination, runs the same loop:

<Steps>
  <Step title="Poll for pending records">
    Ask MerchantOps for records that have been approved and routed to your
    target but not yet reported as done.
  </Step>

  <Step title="Transform">
    Map each MerchantOps record onto whatever shape your system expects
    (field names, units, identifiers).
  </Step>

  <Step title="Push to your system">
    Write the record into your destination — call its API, update your
    database, drop a file, whatever your system needs.
  </Step>

  <Step title="Report the outcome">
    Tell MerchantOps whether each record succeeded, failed, or was skipped.
    Reported records drop out of the next poll; unreported ones come back so
    you can retry.
  </Step>
</Steps>

Today the pull-based connector API covers **pricing** publishing — the loop
applies to price records routed to your target. For catalog publishing, use a
built-in connector such as [VTEX](/publishing/vtex).

## Setup

<Steps>
  <Step title="Register the target">
    In **Settings › Publish Targets**, add an integration for your custom
    target with a target key (for example `custom`) and a **poll** delivery
    mode. No credentials for your system are stored on the MerchantOps side —
    your connector authenticates to your system itself.
  </Step>

  <Step title="Create an API key">
    In **Settings › API Keys**, create a key scoped to your organization with
    the two publishing scopes:

    | Scope                           | Purpose                  |
    | ------------------------------- | ------------------------ |
    | `pricing.publish.outcome.read`  | Poll for pending records |
    | `pricing.publish.outcome.write` | Report outcomes          |
  </Step>

  <Step title="Run your connector">
    Your service holds the key and calls MerchantOps over HTTPS with:

    ```
    Authorization: Bearer <your-api-key>
    ```

    The key is organization-scoped — your connector only ever sees records that
    belong to your organization.
  </Step>
</Steps>

## Poll for pending publishes

Request the records routed to your target that haven't been reported yet:

```bash theme={null}
GET /api/pricing/pending-publishes?target_key=custom&limit=50
Authorization: Bearer <your-api-key>
```

The response lists up to `limit` records with the fields you need to publish
them:

```json theme={null}
{
  "target_key": "custom",
  "count": 1,
  "items": [
    {
      "record_key": "pr-a0861877922e",
      "product_key": "1000035585",
      "product_name": "24/7 Compression Socks",
      "variant_key": "1000035585-001-L2",
      "upc_code": "841052014203",
      "amount": 32.85,
      "currency": "USD",
      "price_type": "map",
      "effective_from": "2026-04-27",
      "batch_key": "pb-16cbebb322bd"
    }
  ]
}
```

Records are deduplicated by outcome state: once you report an outcome for a
record, it drops out of this query. A recommended cadence is a poll every
30–60 seconds, backing off to a few minutes when the response is empty.

## Report an outcome

For each record, POST back what happened:

```bash theme={null}
POST /api/pricing/records/{record_key}/publish-outcome
Authorization: Bearer <your-api-key>
Content-Type: application/json

{
  "target_key": "custom",
  "status": "success",
  "response_code": 200,
  "duration_ms": 145,
  "attempts": 1,
  "external_id": "YOUR-SYSTEM-ID-42"
}
```

<ResponseField name="target_key" type="string" required>
  The target you configured (for example `custom`).
</ResponseField>

<ResponseField name="status" type="string" required>
  One of `success`, `failed`, or `skipped`.
</ResponseField>

<ResponseField name="response_code" type="number">
  The HTTP status or internal result code your system returned. Helps debugging.
</ResponseField>

<ResponseField name="duration_ms" type="number">
  How long your call took.
</ResponseField>

<ResponseField name="attempts" type="number">
  `1` for a first-try success; higher if you retried.
</ResponseField>

<ResponseField name="error" type="string">
  A short failure summary — shown in the publish status UI when the status is
  `failed`.
</ResponseField>

<ResponseField name="external_id" type="string">
  The record's ID in your downstream system, if it has one.
</ResponseField>

The response tells you whether this outcome completed the batch:

```json theme={null}
{
  "record_key": "pr-a0861877922e",
  "target_key": "custom",
  "status": "success",
  "batch_final_status": null
}
```

`batch_final_status` is `null` while other records in the batch are still in
flight, and becomes `"published"` or `"partial"` when your outcome is the one
that finishes the batch.

## Retries and idempotency

Your connector owns its retry logic entirely. A record stays **pending** until
you POST a terminal outcome, so:

* If your connector crashes mid-run, the record is still pending on the next
  poll — just pick it up again.
* Reporting `success` more than once for the same record is safe. Repeat POSTs
  are accepted but don't double-count toward the batch.

## A minimal connector

The whole contract fits in one loop:

```python theme={null}
import os, time, httpx

API_BASE = os.environ["MERCHANTOPS_API_URL"]
API_KEY  = os.environ["MERCHANTOPS_API_KEY"]
TARGET   = "custom"

headers = {"Authorization": f"Bearer {API_KEY}"}

def publish_to_your_system(record: dict) -> tuple[bool, str, int]:
    # Your integration code. Returns (ok, error_message, elapsed_ms).
    ...

def loop():
    with httpx.Client(base_url=API_BASE, headers=headers, timeout=30.0) as client:
        while True:
            resp = client.get(
                "/api/pricing/pending-publishes",
                params={"target_key": TARGET, "limit": 50},
            )
            resp.raise_for_status()
            records = resp.json()["items"]

            if not records:
                time.sleep(60)
                continue

            for r in records:
                ok, err, ms = publish_to_your_system(r)
                outcome = {
                    "target_key": TARGET,
                    "status": "success" if ok else "failed",
                    "duration_ms": ms,
                }
                if not ok:
                    outcome["error"] = err
                client.post(
                    f"/api/pricing/records/{r['record_key']}/publish-outcome",
                    json=outcome,
                )

if __name__ == "__main__":
    loop()
```

## What you get

* **Isolation.** Your connector talks to MerchantOps only over HTTPS. No shared
  infrastructure, no special client libraries.
* **Org scoping.** Your key can only read and update your organization's
  records — there's no way to affect another tenant.
* **The same monitoring surface.** Your publishes appear in
  [publish status](/publishing/publish-status) exactly like built-in
  connectors: per-target breakdown, per-record outcomes, and job progress.

<Warning>
  Rate-limit yourself to roughly two polls per second per connector. Running
  multiple connectors for the same target at once can hand you duplicate records
  (there is no per-worker record lease yet), so run a single connector per target
  for now.
</Warning>

## Next steps

<CardGroup cols={2}>
  <Card title="VTEX (reference connector)" icon="store" href="/publishing/vtex">
    A worked example of a built-in connector for catalog and pricing.
  </Card>

  <Card title="Reading publish status" icon="signal" href="/publishing/publish-status">
    See where your connector's outcomes show up.
  </Card>
</CardGroup>
