---
title: "Joining data"
slug: "joining-data"
description: "Learn how to join data from multiple API endpoints for comprehensive reporting, enhancing insights with one-to-one and one-to-many relationships."
updated: 2026-06-02T11:00:39Z
published: 2026-06-02T11:00:39Z
---

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

# Joining data

Often reports map cleanly to a single API endpoint. However, sometimes the information you want is spread across several endpoints – for example, one endpoint returns a list of campaigns and another returns the performance metrics for each campaign. To deliver this in a single report, you can join data from one or more other report types into it. The parent report supplies the base rows, and each joined report contributes additional fields to those rows.

Data joining happens in the background. The joined reports are usually marked as hidden so the user doesn't see them in the connector – the joined reports add fields to the parent's result and aren't intended to be selected on their own. The end user only ever sees the parent report, with the joined fields appearing alongside its own on each row. A connector can have several visible parent reports, each with its own set of hidden joined reports.

**Example use cases for data joining:**

- The data you need for your report type is returned by more than one endpoint.
  - A summary endpoint returns list of IDs that you want to look fetch details (such as performance stats) for from another endpoint.
- You want to chain related data across multiple endpoints (for example, campaign → line item → daily stats).

## Configuration overview

Joining is defined inside a report type's `decoration` object as an array of `relations`. Each relation describes one join: which report type to fetch from, how to match rows between the two, and how the child request should behave.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `reportTypeId` | string | Yes | The ID of the report type **to join into the current one.** |
| `type` | string | No | How to perform the join: `normal` (default) or `inverse`. |
| `link` | object | Yes | Defines how rows are matched between the two report types. See [Linking rows](/docs/joining-data#linking-rows) below. |
| `useDates` | boolean | No | When `true`, pass the query's date range to the child request. |
| `includeMetrics` | boolean | No | When `true`, the child report's metrics are included alongside its dimensions in the joined result. |
| `inheritDecorations` | boolean | No | When `true`, any joins defined on the child report type are also applied – letting you chain joins across several report types. When `false`, only the child's own data is brought in. |
| `batching` | object | No | Sends multiple parent IDs in a single child request instead of one request per row. See [Batching child requests](/docs/joining-data#batching-child-requests). |

## Linking rows

The `link` object defines how a parent row is matched to a child row. Two patterns are possible, depending on whether the child report can be fetched independently or has to be queried per parent:

- **Match on a shared field** – the parent and child reports both return the same field (typically an ID). Set `parent` to the field ID on the parent report and `child` to the field ID on the child report. The connector fetches the child report's full result and joins rows where the values match.
- **Fetch per parent ID** – the child endpoint requires the parent's ID in its URL or parameters. Set only `parent` (no `child`); the connector fires a child request for each parent row, or for each batch of rows, passing the value in through placeholder `{{query.parent_id}}` or `{{query.batch.parent_ids}}`.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `parent` | string | Yes | The field ID in the parent report type that holds the value used for the join. |
| `child` | string | No | The field ID in the child report type that the parent value must match against. Omit when the child report is fetched per parent ID. |

> **Note:** The `parent` field must be defined in the parent report type's dimensions, even if it is hidden (`"isVisible": false`). It is what carries the join value through to the child request.

## Join types

### One-to-one (`"type": "normal"`)

A one-to-one join is a lookup: for each row in the parent, the matching row from the child is added to the result. The child report typically returns all candidate rows in a single request, and rows are matched by field value (`parent` to `child`, or by fetching child report with `parent` included in the request).

Use the `normal` join type when you want to join data in one-to-one relation, so for example when your parent report returns an ID, and you want to enrich each row with the details for that ID – for example, a campaign row that needs the performance statistics attached.

Below you can find a template for a `normal` join:

```
"decoration": {
  "relations": [
    {
      "reportTypeId": "[child report type ID]",
      "type": "normal",
      "link": {
        "parent": "[parent field ID]",
        "child": "[child field ID]"
      },
      "useDates": true,
      "includeMetrics": true,
      "inheritDecorations": true
    }
  ]
}
```

Below you can find an annotated example from the Basis connector, where the `campaign` report joins each campaign row with its brand details:

```
"decoration": {
  "relations": [
    {
      "reportTypeId": "brand",
      "type": "normal",
      "link": {
        "parent": "campaign_brand_id",
        "child": "brand_id"
      },
      "useDates": true,
      "includeMetrics": true,
      "inheritDecorations": true
    }
  ]
}
```

In this example:

- `reportTypeId` points at the `brand` report type, which fetches the full brand list from `/v1/brands`.
- `link.parent` (`campaign_brand_id`) is the brand ID carried on each campaign row.
- `link.child` (`brand_id`) is the matching field on each brand row.
  - It is advised to mark the child field as hidden (with `"isVisible": false` to avoid displaying duplicate fields.

Each campaign row in the result is enriched with the brand whose `brand_id` matches that row's `campaign_brand_id`.

### One-to-many (`"type": "inverse"`)

An inverse join makes a separate child request for each parent ID, then attaches the results back to the parent. Use this when the child endpoint requires an ID in its URL or parameters and returns its results scoped to that single ID – typical for per-resource performance endpoints, daily breakdowns, or sub-entity lists.

The child report's request can insert the current parent ID using the `{{query.parent_id}}` placeholder.

Below you can find a template for an inverse join:

```
"decoration": {
  "relations": [
    {
      "reportTypeId": "[child report type ID]",
      "type": "inverse",
      "link": {
        "parent": "[parent field ID]"
      },
      "useDates": true,
      "includeMetrics": true,
      "inheritDecorations": false
    }
  ]
}
```

The child report references the parent ID in its request:

```
{
  "id": "[child report type ID]",
  "config": {
    "isVisible": false
  },
  "request": {
    "url": "https://api.example.com/resources/{{query.parent_id}}/performance"
  }
}
```

Below you can find an annotated example from the Ignite connector, where each campaign is joined with its performance time series:

```
"decoration": {
  "relations": [
    {
      "reportTypeId": "campaign_performance",
      "type": "inverse",
      "useDates": true,
      "includeMetrics": true,
      "inheritDecorations": false,
      "link": {
        "parent": "campaign_id"
      }
    }
  ]
}
```

The decorated `campaign_performance` report type injects the parent campaign ID into its URL:

```
{
  "id": "campaign_performance",
  "config": {
    "isVisible": false
  },
  "request": {
    "url": "https://api.realestate.com.au/campaign/v1/display-performance/{{query.parent_id}}"
  }
}
```

For each campaign returned by the parent report, the connector calls `/display-performance/&lt;campaign_id&gt;` and merges the returned rows back into that campaign.

> **Note:** Inverse joins fire one child request per parent row, which can produce a large number of API calls when the parent has many rows. If the child endpoint accepts multiple IDs per request, configure [batching](/docs/joining-data#batching-child-requests) to send them together.

## Batching child requests

Batching sends a group of parent IDs to the child endpoint in a single request, instead of one request per parent row. Use it whenever the child endpoint accepts a list of IDs as a parameter or request body field – it cuts down on API calls and helps avoid rate limits.

Inside the child request, refer to the batch of IDs through the `{{query.batch.parent_ids}}` placeholder. You can format the array using helpers such as `{{implode(',', query.batch.parent_ids)}}` when the endpoint expects a comma-separated list, or use it directly in request body templates.

The `batching` object accepts:

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `isEnabled` | boolean | No | Set to `true` to enable batching for this relation. |
| `batchSize` | integer | No | The number of parent IDs sent in each child request. Tune this to the API's own limit. |

Below you can find a template for an inverse join with batching:

```
"decoration": {
  "relations": [
    {
      "reportTypeId": "[child report type ID]",
      "type": "inverse",
      "link": {
        "parent": "[parent field ID]",
        "child": "[child field ID]"
      },
      "useDates": true,
      "includeMetrics": true,
      "batching": {
        "isEnabled": true,
        "batchSize": 20
      }
    }
  ]
}
```

Below you can find an annotated example from the X (Twitter) connector, where tweet rows are joined with engagement details fetched in batches of 20:

```
"decoration": {
  "relations": [
    {
      "reportTypeId": "details",
      "type": "inverse",
      "includeMetrics": true,
      "useDates": true,
      "link": {
        "parent": "tweet_id",
        "child": "tweet_id"
      },
      "batching": {
        "isEnabled": true,
        "batchSize": 20
      }
    }
  ]
}
```

The decorated `details` report passes the batched IDs as a request parameter:

```
{
  "name": "entity_ids",
  "value": "{{implode(',', query.batch.parent_ids)}}"
}
```

Each call to `details` requests stats for up to 20 tweet IDs at a time, and the returned rows are mapped back to the originating tweets using the `tweet_id` link.

> **Note:** With batching decorations, you always need to define child link as well – otherwise data can't be joined to parent rows.

## Joining multiple child reports

The `relations` array can hold more than one entry, so the same parent report can be enriched with data from several different child reports in a single query. All child reports stay hidden (as long as `"isVisible": false` has been set) – the user still picks only the parent report in the destination, and the merged fields appear on each of its rows.

Below you can find an example from the Basis connector, where the `campaign` report joins brand details (matched on `brand_id`), line items (fetched per campaign), and daily stats (also fetched per campaign):

```
"decoration": {
  "relations": [
    {
      "reportTypeId": "brand",
      "type": "normal",
      "link": { 
        "parent": "campaign_brand_id", 
        "child": "brand_id" 
      },
      "useDates": true,
      "includeMetrics": true,
      "inheritDecorations": true
    },
    {
      "reportTypeId": "line_item",
      "type": "normal",
      "link": { 
        "parent": "campaign_id" 
      },
      "useDates": true,
      "includeMetrics": true,
      "inheritDecorations": true
    },
    {
      "reportTypeId": "stats",
      "type": "inverse",
      "link": { 
        "parent": "campaign_id" 
      },
      "useDates": true,
      "includeMetrics": true,
      "inheritDecorations": true
    }
  ]
}
```

## Chaining joins across report types

A child report type can itself define joins of its own, which lets you chain several report types together. To make those nested joins apply automatically to the parent's result, set `inheritDecorations` to `true` on the parent's relation.

For example, the Basis connector chains `campaign → line item → stats → creative`, with each step adding the next level via its own join. Because every relation enables `inheritDecorations`, requesting the top-level `campaign` report returns rows enriched with line item, stats, and creative information in a single query.

> **Note:** Chaining joins can significantly increase the amount of API calls which might cause problems with API rate limits – avoid long chains when possible.
