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

# Custom Integration

The Custom Integration is for source systems that don't have a first-party Frontic connector — or for cases where you want to push data straight into Frontic from your own code, scripts, or workflow tools. You define the payload shape, push it via the [Ingest API](/reference/ingest-api), and Frontic stores it on the feed as-is. No normalization happens on the connector side; reshaping into your storage schema is the Data Sync's job.

<div style={{ display: "grid", gridTemplateColumns: "repeat(3, 1fr)", gap: "0.75rem", marginTop: "1.5rem", marginBottom: "2rem" }}>
  <div style={{ padding: "0.75rem 1rem", border: "1px solid var(--card-border, rgba(0,0,0,0.08))", borderRadius: "0.625rem" }}>
    <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.04em", opacity: 0.6, marginBottom: "0.25rem" }}>
      <Icon icon="key" iconType="solid" /> Auth
    </div>

    <div style={{ fontSize: "0.875rem", fontWeight: 500 }}>Ingest API token</div>
  </div>

  <div style={{ padding: "0.75rem 1rem", border: "1px solid var(--card-border, rgba(0,0,0,0.08))", borderRadius: "0.625rem" }}>
    <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.04em", opacity: 0.6, marginBottom: "0.25rem" }}>
      <Icon icon="arrows-rotate" iconType="solid" /> Update method
    </div>

    <div style={{ fontSize: "0.875rem", fontWeight: 500 }}>Ingest API only</div>
  </div>

  <div style={{ padding: "0.75rem 1rem", border: "1px solid var(--card-border, rgba(0,0,0,0.08))", borderRadius: "0.625rem" }}>
    <div style={{ display: "flex", alignItems: "center", gap: "0.5rem", fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "0.04em", opacity: 0.6, marginBottom: "0.25rem" }}>
      <Icon icon="boxes-stacked" iconType="solid" /> Resources
    </div>

    <div style={{ fontSize: "0.875rem", fontWeight: 500 }}>Content · Products · Categories</div>
  </div>
</div>

The Custom integration follows the standard integration model — see [how integrations work](/connectors/overview) for Connection, Channels, and Data Feeds. This page covers what's specific to it.

## Connection settings

There's nothing to enter on the connection side — the Custom integration's "connection" is the [Ingest API](/reference/ingest-api). Authentication uses an Ingest API token.

## Channels

Channels for a Custom integration are pure containers for translations and (optionally) source-side channel keys you reference in payloads. There's no source system to validate against, so you define them yourself — Channel Name, Available Translations, Fallback Translation. See [Channels in the overview](/connectors/overview#channels) for the framework-level fields.

## Data Feeds

The Custom integration exposes three feed types:

| Feed           | What it carries                                                                         |
| -------------- | --------------------------------------------------------------------------------------- |
| **Content**    | Generic records with the structure you define.                                          |
| **Products**   | Records with optional parent/variant linking via the configured parent-source-id field. |
| **Categories** | Generic records intended for category data.                                             |

The standard **Settings → Updates → Schema** setup wizard applies — see [Data Feeds in the overview](/connectors/overview#data-feeds). For Custom integrations specifically:

### Per-feed config

<ParamField path="Source ID Field" type="string" required default="id">
  The name of the JSON key in your payload that carries the record's Source ID. Defaults to `id`.
</ParamField>

<ParamField path="Parent Source ID Field" type="string" required default="parentId">
  *(Products feed only)* The name of the JSON key in your payload that carries the parent product's Source ID, used to link variants. Defaults to `parentId`.
</ParamField>

### Update methods

* **Ingest API only.** Push records to the feed's HTTPS endpoint using the [Ingest API](/reference/ingest-api). Webhooks and scheduled polling don't apply here — your code controls when records arrive.

## Record structure

Records arrive as JSON. The connector recognizes three special keys for context-specific values: `@channel`, `@translation`, and `@currency`. You can use any combination on the same record — Frontic resolves them at sync time.

<Note>
  **Reserved key prefixes.** The `@`-prefix namespace above (`@channel`, `@translation`, `@currency`) and the `$`-prefix namespace used by other connectors (`$rel`, `$options`, `$properties` — see the [overview](/connectors/overview#reserved-prefix-namespace)) are both reserved for Frontic's framework. Don't use them as keys for your own custom data — they'll be interpreted or overwritten.
</Note>

### Channel-specific data

Use `@channel` and the key of your integration's channels to define data that differs per channel.

```json theme={"theme":"css-variables"}
{
  "id": 1,
  "productNumber": "my-product-1234",
  "name": "My Product",
  "categoryIds": [14, 23, 30],
  "@channel": {
    "b2c": {
      "technicalProductName": "My Product (Revision 241)"
    },
    "b2b": {
      "technicalProductName": "AA-241-1234"
    }
  }
}
```

The value for `name` stays the same across channels; `technicalProductName` is per channel.

### Translation-specific data

Use `@translation` and a locale key for per-locale values:

```json theme={"theme":"css-variables"}
{
  "id": 1,
  "productNumber": "my-product-1234",
  "categoryIds": [14, 23, 30],
  "@translation":{
    "de-DE":{
      "name": "Produkt 1",
      "description":"Ein Text"
    },
    "en-GB":{
      "name": "Product 1",
      "description":"Some Text"
    }
  }
}
```

### Currency-specific data

Use `@currency` and an ISO currency code for per-currency values (typically prices):

```json theme={"theme":"css-variables"}
{
  "id": 1,
  "productNumber": "my-product-1234",
  "categoryIds": [14, 23, 30],
  "@currency": {
    "EUR": {
      "productPrice": {
        "gross": 19.99,
        "net": 16.79,
        "currency": "EUR",
        "precision": 2
      }
    },
    "USD": {
      "productPrice": {
        "gross": 19.99,
        "net": 19.99,
        "currency": "USD",
        "precision": 2
      }
    }
  }
}
```

### Combining annotations

You can combine `@channel`, `@translation`, and `@currency` on the same record. Frontic resolves them in the order **channel → currency → translation** so per-channel currency overrides apply correctly.

## Product variants

Products in a Custom integration support a parent-child structure. The variant relationship uses two field names from your payload: the **Source ID field** and the **Parent Source ID field**, both configurable on the feed.

There are three product types, distinguished by how the two fields relate:

<Tabs>
  <Tab title="Basic product (no variants)">
    Set parent Source ID equal to the Source ID — the record is its own parent.

    ```json theme={"theme":"css-variables"}
    {
      "id": "my-product-123",
      "parentId": "my-product-123"
    }
    ```
  </Tab>

  <Tab title="Main product (has variants)">
    Set parent Source ID to an empty string. The record is the parent of its variants.

    ```json theme={"theme":"css-variables"}
    {
      "id": "my-product-456",
      "parentId": ""
    }
    ```
  </Tab>

  <Tab title="Variant product">
    Set parent Source ID to the parent product's Source ID.

    ```json theme={"theme":"css-variables"}
    {
      "id": "my-product-123-red-small",
      "parentId": "my-product-123"
    }
    ```
  </Tab>
</Tabs>

Records can arrive in any order — variants before their parents are fine; Frontic links them once both are present.

## Bulk upserts

Push a JSON array to the Ingest API to upsert many records in one call:

```json theme={"theme":"css-variables"}
[
  {
    "id": "my-first-product"
  },
  {
    "id": "my-second-product"
  }
]
```

Each array element is treated as a separate record with the same rules as a single-record push.

## Troubleshooting

When records you've pushed aren't where you expect them to be, work through these checks in order:

<Steps>
  <Step title="Confirm the records reached the feed" icon="inbox">
    Open the Data Feed and click **Recent Records**. The last records the connector accepted appear with their Source ID, timestamp, and full payload. If your latest push isn't there, the records didn't make it past intake — check the Ingest API response for a `400` or `401`, and confirm the Source ID field is present in every payload.
  </Step>

  <Step title="Confirm the payload looks the way you expect" icon="eye">
    Click into a record. Verify the field names and shapes match what your Data Sync expects — typos in keys, accidentally-stringified numbers, or missing context annotations (`@channel`, `@translation`, `@currency`) are the usual culprits.
  </Step>

  <Step title="Check the Data Sync setup" icon="link">
    A feed without a [Data Sync](/data-integration/data-storages#populate-a-storage) connecting it to a storage stays a feed — records never become storage records. Confirm the sync exists and points at the right feed.
  </Step>

  <Step title="Check the locale mapping on the sync" icon="globe">
    The Data Sync's **Channel → Scope mapping** decides which translations land in which project locale. Make sure the integration's channels are mapped to your scopes, and the channel's translations are mapped to the project locales — including the **fallback translation** the channel was configured with. Skipping the fallback is the single most common cause of "translations not syncing" issues with custom integrations.
  </Step>

  <Step title="Look for transformation errors" icon="triangle-exclamation">
    If records reached the feed but a sync run produced a notification like *Storage Record Transformation Error*, the [Value Composer](/data-integration/value-composer) chain hit a runtime issue on a specific record. Open the **Record Debugger** in the feed sync panel to walk the chain on the offending record and pinpoint which slot produced the wrong value — see [Error handling](/reference/value-composer#error-handling) for the kinds of failures it surfaces.
  </Step>
</Steps>

## Good to know

* **You own the schema.** No source-side normalization happens — what you push is what arrives on the feed. Reshape into your storage schema in the [Value Composer](/data-integration/value-composer).
* **Records without a Source ID are dropped.** Confirm every payload carries the configured Source ID field; missing IDs are rejected silently at the feed level.
* **Change detection applies.** Re-pushing an identical record short-circuits past intake — see [change detection](/connectors/overview#change-detection) in the overview.

## Related

<CardGroup cols={2}>
  <Card title="Ingest API" icon="upload" href="/reference/ingest-api">
    The HTTP endpoints, payload shape, and auth model.
  </Card>

  <Card title="n8n" icon="diagram-project" href="/connectors/n8n">
    Pushing into a Custom integration from an n8n workflow.
  </Card>
</CardGroup>
