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

# Crosspost groups

> Inspect, create, and correct groups of tracked posts across platforms.

Crosspost groups connect versions of the same content posted to different
platforms by one campaign creator. These endpoints group existing tracked
posts; they do not publish content to social networks.

Use the `campaignCreatorRecordId` returned by the
[campaign creator list](/campaign-creators/list). Every request requires
`X-Application-Id` and `X-Api-Key`. The record, target group, and all supplied
posts must belong to your agency and the same campaign creator.

## Automatic grouping and caption matching

Automatic detection normally uses your agency's detection settings, such as
the upload window and duration tolerance in `UPLOAD_WINDOW` mode. A contract's
caption grouping rule changes how matching posts are grouped.

Set `contract.crosspostRequirements.captionContains` when
[creating a contract version](/campaign-creators/create-contract-version).
In the [complete terms snapshot](/campaign-contracts), the equivalent field is
`crosspost.captionFilter`. The contract version governing each post's upload
business date supplies this rule.

For example, `"captionContains": "#launch"` groups matching posts for the
same campaign creator and the same configured filter by **local calendar
day**. Grade determines that day from the post's upload timestamp using:

1. The campaign's `timeZone`, when set.
2. The agency's timezone, when the campaign has no override.
3. UTC, when neither is set.

Set the campaign override through the [campaign endpoints](/campaigns).
This is a midnight-to-midnight calendar day, including daylight-saving
changes. It is not a rolling 24-hour window. For example, with
`America/Los_Angeles`, uploads at `2026-09-17T23:30:00Z` and
`2026-09-18T01:00:00Z` both belong to September 17 locally. Uploads on
opposite sides of local midnight belong to different caption groups,
even when they are only minutes apart.

<Warning>
  Caption grouping overrides the normal upload-window and duration-tolerance
  checks for matching posts. With a six-hour window configured, two matching
  posts 20 hours apart can still join the same group if they share the local
  calendar day. This rule also allows multiple posts from the same platform.
  Reusing a campaign-wide hashtag for several distinct videos in one day can
  therefore combine those videos and affect payments configured per group.
</Warning>

Matching is case-insensitive, and a leading `#` or `@` in a token is ignored.
Comma-separated tokens match any listed token; they do not create separate
groups for each token. Posts that do not match
continue through normal automatic detection. Set `captionContains` to `null`
to disable this override in a contract version.

If you only want to choose which posts to track from an account, use
[`media[].captionFilter`](/campaign-media) when adding that account. That
tracking filter does not enable caption-based grouping. To correct an
existing group, use the manual membership and split operations below.

## Reads

The base path for the five endpoints below is:

```text theme={null}
/sdk/v1/campaign-creators/:campaignCreatorRecordId/crosspost-groups
```

### GET /

List groups ordered by group ID. Query parameters are `page` (default `1`,
maximum `1000000`) and `limit` (default `50`, maximum `100`). Both must be positive
whole numbers. Unknown parameters are rejected.

```bash theme={null}
curl -sS "$BASE_URL/sdk/v1/campaign-creators/CCR_123/crosspost-groups?limit=50" \
  -H "X-Application-Id: $APP_ID" \
  -H "X-Api-Key: $API_KEY"
```

```json theme={null}
{
  "success": true,
  "data": [
    {
      "crosspostGroupId": "xpg_468b922199df4a3a97df1c72c9a75bce",
      "crosspostGroupName": "1-Sep-2026",
      "postsCount": 2,
      "platforms": ["INSTAGRAM", "TIKTOK"],
      "source": "MANUAL"
    }
  ],
  "pagination": { "page": 1, "limit": 50, "total": 1, "totalPages": 1 },
  "membershipVersion": "b770693c6069476802f6c5b047937701047849a0b456354a9e328d93e12a0c62"
}
```

`source` is `AUTO`, `MANUAL`, `MIXED`, or `null` for legacy groups without a
recorded source. `MIXED` means members have different sources. Names are
generated display labels; use the opaque group ID to reference a group.
Automatic groups and names can change after detection runs.

The existing [creator media endpoint](/campaign-media) still supports
`includeGroups=true` and returns richer media/performance summaries. This direct
list provides independent group pagination and a version for safe edits.

### GET /:crosspostGroupId/posts

Read a group's members ordered by media ID, with the same `page` and `limit`
parameters. Both directly tracked posts and posts discovered from tracked
accounts are included. Account rows, deleted posts, and retired posts are excluded.

```bash theme={null}
curl -sS "$BASE_URL/sdk/v1/campaign-creators/CCR_123/crosspost-groups/$GROUP_ID/posts" \
  -H "X-Application-Id: $APP_ID" \
  -H "X-Api-Key: $API_KEY"
```

Each item in `data` includes:

| Field | Meaning |
| - | - |
| `campaignCreatorMediaId` | Tracked post ID used for membership writes |
| `crosspostGroupId`, `crosspostGroupName` | Current group identity and display name |
| `crosspostGroupSource` | This member's `AUTO` or `MANUAL` source; may be `null` |
| `platform`, `mediaUrl`, `postId` | Social post identity |
| `title`, `captionText`, `authorUsername`, `uploadDate` | Available post metadata |
| `sourceCampaignCreatorMediaId` | Tracked account that discovered the post, or `null` |
| `views`, `likes`, `comments` | Latest tracked metrics; zero when no metrics exist |
| `metricsAsOfTimestamp` | Latest metric collection time, or `null` |

The response also includes `pagination` and `membershipVersion`, as in the group
list. Missing or out-of-scope groups return `404`. A valid group with an
out-of-range page returns an empty `data` array.

## Writes and retries

All three writes require `X-Idempotency-Key` (1–255 characters). Generate a key
once for each logical operation and reuse it with the same body after a timeout
or retryable error. Completed responses are retained for 24 hours, scoped to
your application, agency, campaign creator, and operation. A replay returns the
original status/body and `X-Idempotent-Replay: true`; a changed payload with the
same key returns `409 IDEMPOTENCY_CONFLICT`.

Optionally send `expectedMembershipVersion` from a read or the previous write.
A stale version returns `409 MEMBERSHIP_CONFLICT` without changing any posts.
Read again and submit your revised operation with a new key. The version covers
the creator's active membership; it is separate from scrape freshness.

Each batch is atomic. If any post or target group is invalid, no membership
changes are committed. Writes reject account rows, deleted/retired posts, and
completed campaigns. Each write supports creators with up to 5,000 active posts;
create and membership batches accept at most 500 distinct media IDs.

### POST /

Create a populated group from a non-empty `campaignCreatorMediaIds` array.
A post may move from an existing group. A one-post manual group is allowed;
its existence does not establish payout eligibility.

```bash theme={null}
curl -sS "$BASE_URL/sdk/v1/campaign-creators/CCR_123/crosspost-groups" \
  -X POST \
  -H "Content-Type: application/json" \
  -H "X-Application-Id: $APP_ID" \
  -H "X-Api-Key: $API_KEY" \
  -H "X-Idempotency-Key: $IDEMPOTENCY_KEY" \
  -d '{"campaignCreatorMediaIds":["CCMEDIA_123","CCMEDIA_456"]}'
```

Returns `201` with a server-assigned group ID. Creation and all assignments happen
together; no separate request is needed to attach the posts.

### PATCH /memberships

Send a non-empty `assignments` array. Every media ID must appear once. Choose
one of these states for each post:

| State | Assignment fields |
| - | - |
| Manually assign or move a post | `mode: "MANUAL"` and an existing `crosspostGroupId` |
| Keep a post ungrouped | `mode: "MANUAL"` and `crosspostGroupId: null` |
| Return a post to automatic detection | `mode: "AUTO"`; omit `crosspostGroupId` |

```json theme={null}
{
  "assignments": [
    {
      "campaignCreatorMediaId": "CCMEDIA_123",
      "mode": "MANUAL",
      "crosspostGroupId": "xpg_468b922199df4a3a97df1c72c9a75bce"
    },
    {
      "campaignCreatorMediaId": "CCMEDIA_456",
      "mode": "MANUAL",
      "crosspostGroupId": null
    },
    {
      "campaignCreatorMediaId": "CCMEDIA_789",
      "mode": "AUTO"
    }
  ]
}
```

Automatic detection runs after the assignments. A manual exclusion survives
future recomputes. Resetting to `AUTO` removes that override and evaluates the
post immediately; it may remain ungrouped if there is no match or insufficient
metadata. An ungrouped automatic candidate can have a `null` source.

To dissolve a group, manually ungroup all its members. This keeps the tracked
content. To split a group, create a new group from the selected members.

### POST /recompute

Rerun automatic grouping for this campaign creator. Send `{}` or an object
containing `expectedMembershipVersion`. Uses the agency's current detection
settings and applicable contract caption rules. Caption grouping uses the
current campaign → agency → UTC timezone fallback described above, so changing
the timezone can change automatic membership on the next recompute. Manual
assignments and exclusions are preserved. This operation does not scrape fresh
post metadata or reset manual overrides.

### Write response

All writes complete membership changes before responding. Creation also includes
`crosspostGroupId`; membership updates and recompute return `200`.

```json theme={null}
{
  "success": true,
  "data": {
    "campaignCreatorRecordId": "CCR_123",
    "crosspostGroupId": "xpg_468b922199df4a3a97df1c72c9a75bce",
    "memberships": [
      {
        "campaignCreatorMediaId": "CCMEDIA_123",
        "crosspostGroupId": "xpg_468b922199df4a3a97df1c72c9a75bce",
        "crosspostGroupName": "1-Sep-2026",
        "crosspostGroupSource": "MANUAL",
        "platform": "INSTAGRAM",
        "mediaUrl": "https://www.instagram.com/p/example"
      }
    ],
    "membershipVersion": "b770693c6069476802f6c5b047937701047849a0b456354a9e328d93e12a0c62",
    "recompute": { "updated": 0, "skippedManual": 1, "skippedMissing": 0 },
    "status": "COMPLETED",
    "summaryRefresh": "QUEUED"
  }
}
```

`memberships` contains the posts supplied to create/update, in media-ID order.
For recompute it is empty: use the reads to fetch current groups and members.
`recompute.updated` counts automatic changes, not the supplied manual assignments.
The operation may also change other automatic groups in this creator record.

Calculated dashboard summaries refresh through a durable background queue.
For monetary results, use the existing
[line items](/campaign-creators/line-items) or
[payout preview](/campaign-creators/payout-preview) endpoints. Group membership
changes preserve established payment ownership. A merge of posts with different
established payment-group owners returns `409 PAYMENT_GROUP_CONFLICT` and rolls
back. Safe splits and manual exclusions preserve their original payment history.
Configure payment rules through [contract versions](/campaign-creators/create-contract-version).

## Error responses

Errors use `{ "success": false, "code": "...", "message": "..." }`.
Authentication failures use the standard SDK authentication response.

| Status | Code | Action |
| - | - | - |
| `400` | `INVALID_REQUEST`, `INVALID_BATCH`, `DUPLICATE_MEDIA` | Correct fields, pagination, ID/key length, or duplicate media IDs |
| `401` / `403` | Standard SDK auth error | Check credentials, API enablement, and origin |
| `404` | `NOT_FOUND`, `MEDIA_NOT_FOUND`, `GROUP_NOT_FOUND` | Use a record, active post, and existing target group in your authorized scope |
| `409` | `CAMPAIGN_COMPLETED` | Use an editable campaign |
| `409` | `MEMBERSHIP_CONFLICT` | Read current membership and submit a revised operation with a new key |
| `409` | `IDEMPOTENCY_CONFLICT` | Use the original request for a retry; use a new key for a different operation |
| `409` | `PAYMENT_GROUP_CONFLICT` | Keep the established payment groups separate |
| `409` | `RETRY_OPERATION` | Retry the same body and key after a short backoff |
| `413` | `RECOMPUTE_LIMIT` | The creator exceeds the 5,000-active-post write limit |
| `500` | `INTERNAL_ERROR` | Retry writes using the original key |


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