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

# Build an Extole to Partner Integration

> Install a maintained partner source, reshape it to the shape the partner page defines, and attach its webhooks and credential.

Part of the [partner integration guide](/technical/building-partner-integrations/integration-lifecycle/management-api-integration).

# Overview

An Extole to Partner integration forwards Extole program activity out to a partner platform. Extole maintains a source for most partners in this direction, so the build is an install and a reshape rather than a construction.

Substitute the component name, endpoints, and tag namespace from the partner page throughout. [Build a Partner Integration with the Management API](/technical/building-partner-integrations/integration-lifecycle/management-api-integration) covers the constraints and shared mechanics every build follows.

## When You Would Build One

| You want                                                         | Partner platform                | The install adds                                       |
| :--------------------------------------------------------------- | :------------------------------ | :----------------------------------------------------- |
| Extole activity to trigger a partner message                     | Marketing automation, messaging | A message-trigger webhook and its `WEBHOOK_ID` setting |
| Extole activity to keep partner profiles or audiences current    | Customer data platform          | An ingestion webhook and its `WEBHOOK_ID` setting      |
| Extole program activity in a partner's reporting                 | Analytics                       | An ingestion webhook and its `WEBHOOK_ID` setting      |
| A marketing campaign to attach partner actions to its own events | Any partner in this direction   | A typed data-item child of the integration component   |

Do not add Partner to Extole business-event scaffolding to this install. A partner that orders gift cards, prepaid cards, points, or payouts uses the distinct [reward fulfillment model](/technical/building-partner-integrations/integration-types/integration-build-reward-fulfillment).

## Before You Start

| Parameter                  | Value                                                                              |
| :------------------------- | :--------------------------------------------------------------------------------- |
| `TOKEN`                    | Server-side access token authorized to manage campaigns, components, and webhooks. |
| `EXTOLE_API_HOST`          | Production host for those calls.                                                   |
| `PARTNER_COMPONENT_NAME`   | Maintained component name from the partner page.                                   |
| `SOURCE_COMPONENT_ID`      | The Extole-owned maintained source from the duplicatable listing.                  |
| `INTEGRATION_COMPONENT_ID` | The installed integration component, for component-scoped webhooks.                |

Read the partner page first — it names the finished tree, the endpoints, and the tag namespace — and confirm the duplicatable listing holds a maintained integration component whose name matches the partner. Without one, build from [Create the Integration Campaign and Component Model](/technical/building-partner-integrations/integration-lifecycle/integration-component-model) instead.

## How to Build

### Confirm the Finished Shape

The partner page's product description specifies the finished tree. Each statement it makes maps to something the install must carry:

| What the partner page states                                     | What the install must carry                                                |
| :--------------------------------------------------------------- | :------------------------------------------------------------------------- |
| The activity the integration forwards                            | One child per listed activity, and none forwarding activity the page omits |
| The partner endpoints Extole calls                               | One webhook per endpoint, each tagged by purpose                           |
| That program campaigns attach partner data to their own events   | A typed data-item child of the integration component                       |
| That the integration exposes its partner connections as settings | One `WEBHOOK_ID` setting per webhook, resolved by tag                      |
| The account URL and credential the partner requires              | The matching settings on the integration component                         |

Read that mapping as exhaustive rather than as a minimum, and apply it to an integration already in the account as much as to a fresh install. A maintained source ships the union of what every account might want, so it commonly installs children the page does not list and only one of the webhooks it names.

A maintained source may not ship the report-runner and event-stream views every integration carries. Read the installed `views` socket and add whichever is missing from [Add the Activity and Event Views](/technical/building-partner-integrations/integration-lifecycle/integration-activity-views).

### Create Missing Component Types

A typed child needs its component type first, and a partner page can require a type the account has never used:

```bash theme={null}
curl -s -H "Authorization: Bearer ${TOKEN}" \
  "${EXTOLE_API_HOST}/v1/component-types/${PARTNER_COMPONENT_NAME}-data"

curl -s -X POST -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"name":"'"${PARTNER_COMPONENT_NAME}"'-data","display_name":"Partner Data Item","schema":"{}"}' \
  "${EXTOLE_API_HOST}/v1/component-types"
```

Omit `parent`. An empty `types` array leaves an untyped component, which satisfies no socket filter and no template lookup.

### Install the Maintained Source

An install is `POST /v1/components/{SOURCE_COMPONENT_ID}/duplicate` without `target_campaign_id`: omitting the target campaign creates a new root integration campaign that copies the source tree, including its webhooks and child controllers. Send a body carrying at least one property, such as `component_display_name`.

List the candidates before duplicating anything:

```bash theme={null}
CANDIDATES=$(curl -s -H "Authorization: Bearer ${TOKEN}" --get \
  "${EXTOLE_API_HOST}/v1/components/duplicatable" \
  --data-urlencode "version_state=PUBLISHED" \
  --data-urlencode "show_all=true" \
  | jq --arg name "${PARTNER_COMPONENT_NAME}" \
      '[.[] | select(.name == $name)
             | select(any((.types // [])[]; startswith("integration-v10")))]')

jq -r '.[] | "\(.id)\t\(.types | join(","))"' <<< "${CANDIDATES}"
```

Choose the Extole-owned source.

```bash theme={null}
curl -s -X POST -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"component_display_name":"Partner"}' \
  "${EXTOLE_API_HOST}/v1/components/${SOURCE_COMPONENT_ID}/duplicate"
```

### Reshape the Install

The reshape uses these calls, each needing a campaign version read immediately beforehand:

| Action                    | Call                                                                                    |
| :------------------------ | :-------------------------------------------------------------------------------------- |
| Delete an installed child | `DELETE /v2/campaigns/{campaign_id}/version/{version}/components/{component_id}`        |
| Create a child            | `POST /v2/campaigns/{campaign_id}/version/{version}/components`                         |
| Add or change a setting   | `POST /v2/campaigns/{campaign_id}/version/{version}/components/{component_id}/settings` |
| Create a webhook          | `POST /v6/webhooks`                                                                     |
| Publish the campaign      | `POST /v2/campaigns/{campaign_id}/version/{version}/publish`                            |

Bring the installed tree to the partner page's shape in one pass:

* Delete the installed children the partner page does not keep.
* Create the children it adds, including any typed data template.
* Remove parent settings that belonged to a deleted child, such as a trigger-event-name setting whose controller is gone.
* Set one `WEBHOOK_ID` setting per partner endpoint, resolved by webhook tag rather than by identifier, so the setting survives a rebuild:

```javascript theme={null}
javascript@buildtime: (function() { var filteredElements = Java.from(context.getComponent().createElementsQuery().withType('WEBHOOK').withTag('internal:partner:message-trigger').list()); return filteredElements && filteredElements.length > 0 ? filteredElements[0].getId() : null; })();
```

Filter on the purpose tag exactly one webhook carries — here `internal:partner:message-trigger`, matching the webhook below. Never use a shared tag such as `internal:partner`, which matches every webhook the integration owns.

A partner data template is a typed child of the integration component, created through `component_ids` with no socket. Its install expression anchors the source component's unanchored step data onto the target event:

```javascript theme={null}
javascript@installtime:const sourceData = Java.from(context.getSourceComponent().getUnanchoredStepData());
let targetSteps = Java.from(context.getTargetComponent().getSteps());
const stepName = context.getVariableContext().get("step");

if (stepName !== undefined && stepName !== null) {
    targetSteps = targetSteps.filter(function (step) {
        return step.getName() === stepName;
    });
}


if (targetSteps.length) {
    for (var i = 0; i < sourceData.length; i++) {
        targetSteps[0].anchor(sourceData[i]);
    }

    return;
}
```

### Publish, Then Attach Component-Scoped Webhooks

Publish the campaign once before creating any webhook whose name or URL expression calls `context.getComponent()`, and create those webhooks with `component_ids` naming the integration component. The reshape cannot complete without that publish, so get any release approval first.

<Warning>
  A published integration campaign has no supported route back to a draft: there is no stop or unpublish action, and archiving takes the integration out of use entirely.
</Warning>

Publishing validates every webhook the campaign already owns, so keep a valid placeholder host in any account-URL setting that feeds a webhook URL. The maintained source's own default is one. Set `enabled` to `false` while a webhook points at that placeholder — enabled, it sends live program data to a host that should never receive it, unsigned when the client key is also missing. Enabling it is the step that puts the partner connection into service.

`POST /v6/webhooks`:

```json theme={null}
{
  "name": "javascript@buildtime:context.getComponent().getName() + '_message_trigger'",
  "url": "javascript@buildtime:context.getVariableContext().get('partnerRestUrl') + '/partner/endpoint/path'",
  "type": "GENERIC",
  "default_method": "POST",
  "enabled": "javascript@buildtime:context.getVariableContext().get('enabled')",
  "client_key_id": "javascript@buildtime:context.getVariableContext().get('clientKeyId')",
  "request": "javascript@runtime:context.createRequestBuilderWithDefaults().withUserAgent('partner-Extole-Integration/1.0').build();",
  "retry_intervals": [1, 30, 60],
  "tags": ["internal:partner:message-trigger", "internal:partner"],
  "component_ids": ["INTEGRATION_COMPONENT_ID"]
}
```

Name each webhook for the endpoint it calls: an ingestion endpoint and a message-trigger endpoint are separate webhooks with separate tags. Keep the broad `internal:partner` tag for listing every webhook the integration owns. Where the account URL setting may lack a scheme, build the URL expression to add `https://`.

### Attach the Credential

Create the webhook client key once you have the partner's API secret, then set the credential setting on the integration component. A missing credential does not block the build: leave the setting null and track it as outstanding.

## Error Handling

| Response                                          | Cause                                                                                                                                                                 | What to do                                                                           |
| :------------------------------------------------ | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------- |
| `missing_request_body`                            | The duplicate request had no body.                                                                                                                                    | Send a JSON object with at least one property, such as `component_display_name`.     |
| Unrecognized property                             | The body used `display_name`.                                                                                                                                         | Use `component_display_name`.                                                        |
| `invalid_null`                                    | `target_campaign_id` was sent as null.                                                                                                                                | Omit the attribute. An install creates a new campaign by leaving it off.             |
| `invalid_component_reference`                     | A webhook calling `context.getComponent()` was created before the campaign was published.                                                                             | Publish once, then create the webhook with `component_ids`.                          |
| Campaign validation rejects the publish           | An account-URL setting that feeds a webhook URL is empty or not a valid host.                                                                                         | Keep a valid placeholder host there.                                                 |
| Webhook setting evaluates to `null`               | The `WEBHOOK_ID` expression filters on a tag no webhook carries, or several webhooks share the tag and `[0]` is arbitrary.                                            | Give each webhook its own purpose tag.                                               |
| The first duplicatable result is the wrong source | The listing returns the maintained source plus one copy per account that installed it, in no reliable order, so `head -n 1` can install a client's own configuration. | Choose the Extole-owned source. Stop and ask when two entries are indistinguishable. |
| A typed child is rejected                         | Its component type does not exist in the account.                                                                                                                     | Create the component type first; do not fall back to an empty `types` array.         |

## How to Test

A `2xx` on the duplicate call means the source tree was copied, not that the install matches the partner page. Read the campaign and its `/v6/webhooks` entries back, then confirm:

* The tree matches the partner page: one child per activity the page lists, and no child forwarding activity it does not.
* Every typed child carries its type.
* Each webhook exists with its tags and its resolved URL.
* Each `WEBHOOK_ID` setting resolves to a webhook identifier rather than `null`.
* `enabled` is `true` on every webhook whose real URL and credential are both configured, and `false` on the rest.

Then trigger the Extole event the integration forwards and confirm the webhook fired against the partner endpoint. Record the events it forwards and any credential or partner-side permission still outstanding.

## Related Documentation

* [Integration Categories](/technical/building-partner-integrations/integration-types/integration-categories)
* [Validate and Publish an Integration](/technical/building-partner-integrations/integration-lifecycle/integration-validation)
