> ## 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

> Forward Extole activity to a partner by installing a maintained source or building the controllers and webhooks its API requires.

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 to a partner platform. Start from a maintained source when it includes the required controllers and webhooks. When the source contains only display metadata, add the controller-backed delivery yourself.

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, when one exists. |
| `INTEGRATION_COMPONENT_ID` | The installed or newly created integration component, for component-scoped webhooks. |
| `CAMPAIGN_ID`, `CAMPAIGN_VERSION` | The integration campaign and its current version. Every change creates a new version, so read the version again before each versioned call. |
| `CONTROLLER_ID` | The controller that listens for one class of Extole activity. |
| `WEBHOOK_ID` | The outbound connection invoked by a controller action. |

Read the partner page first — it names the finished tree, the endpoints, and the tag namespace — and inspect any matching source before choosing the build path. Without a complete source, create the container from [Create the Integration Campaign and Component Model](/technical/building-partner-integrations/integration-lifecycle/integration-component-model), then add the controller-backed delivery described below.

### Choose the Build Path

List the duplicatable sources whose name matches the partner exactly, accepting every `integration-v10.*` revision — [Install the Maintained Source](#install-the-maintained-source) shows the query — then inspect the integration component and its children.

The integration component's `integration-v10.*` type tells you the version, not whether the source delivers anything. A complete maintained source has all of the following:

* An integration component tagged `internal:self-managed`
* Child controllers for the activity it forwards
* Webhook actions with event-specific data
* The webhooks and credential settings those actions reference

When the source carries only display metadata, use the controller-backed path below.

Choose each controller's trigger by the kind of activity it forwards:

| Activity | Trigger |
| :- | :- |
| Share-link creation | An event trigger with `event_type` set to `SHAREABLE`. The standard event name is `advocate_code_created`. |
| A program outcome such as `converted` | An event trigger with `event_type` set to `STEP`. |
| A fulfilled reward | A reward-event trigger with `reward_states` set to `FULFILLED`, created through `.../controllers/{controller_id}/triggers/reward-events`. |

Share-link creation and `shared` are distinct. The first creates a shareable and exposes its link; the second records a share action.

Forwarding a fulfilled reward to a messaging or analytics partner is not the same as a partner that supplies the reward. A gift card, prepaid card, points, or payout provider uses [Build a Reward Fulfillment Integration](/technical/building-partner-integrations/integration-types/integration-build-reward-fulfillment).

## 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 Controller-Backed Outbound Delivery

Skip this section when the maintained source already contains the requested controllers and payloads.

Create one controller per event class or payload contract. Attach the controller to the integration component with `component_ids`; do not modify a marketing campaign to host integration behavior.

Create a controller:

```bash theme={null}
curl -s -X POST -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "CONTROLLER",
    "name": "send_converted_to_partner",
    "scope": "PRIVATE",
    "enabled": false,
    "enabled_on_states": ["LIVE", "PAUSED"],
    "selectors": ["TARGET"],
    "journey_names": ["participant"],
    "component_ids": ["'"${INTEGRATION_COMPONENT_ID}"'"],
    "data": [
      {
        "name": "recipient_email",
        "value": "spel@runtime:context.person.email",
        "scope": "PRIVATE",
        "persist_types": ["STEP"]
      }
    ]
  }' \
  "${EXTOLE_API_HOST}/v2/campaigns/${CAMPAIGN_ID}/version/${CAMPAIGN_VERSION}/controllers"
```

Record the returned controller identifier as `CONTROLLER_ID`.

Refresh the campaign version, then add a step-event trigger:

```bash theme={null}
curl -s -X POST -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "trigger_phase": "MATCHING",
    "trigger_name": "Converted",
    "event_names": ["converted"],
    "event_type": "STEP"
  }' \
  "${EXTOLE_API_HOST}/v2/campaigns/${CAMPAIGN_ID}/version/${CAMPAIGN_VERSION}/controllers/${CONTROLLER_ID}/triggers/events"
```

For share-link creation, use the same endpoint with `event_type` set to `SHAREABLE` and read the link from `context.causeEvent.shareable.link`.

Create every controller and trigger while the path is disabled. Then publish and create the component-scoped webhook, as described in [Publish, Then Attach Component-Scoped Webhooks](#publish-then-attach-component-scoped-webhooks), and attach it to the controller through [Attach the Webhook Action](#attach-the-webhook-action). Take field names and identity rules from the partner page.

### 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. Read the type, and create it only when the read finds nothing:

```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": false,
  "client_key_id": "javascript@buildtime:context.getVariableContext().get('clientKeyId')",
  "request": "javascript@runtime:context.createRequestBuilderWithDefaults().withUserAgent('partner-Extole-Integration/1.0').build();",
  "response_handler": "javascript@runtime:(function(){ var statusCode = context.getWebhookDispatchResultEvent().getResponseStatusCode(); if (statusCode >= 200 && statusCode < 300) { return 'OK'; } if (statusCode == null || statusCode === 429 || statusCode >= 500) { return 'RETRY'; } context.log('Partner rejected webhook request with status ' + statusCode); return 'OK'; })()",
  "retry_intervals": [1, 30, 60],
  "tags": ["internal:partner:message-trigger", "internal:partner"],
  "component_ids": ["INTEGRATION_COMPONENT_ID"]
}
```

Record the returned webhook identifier as `WEBHOOK_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 Webhook Action

Refresh the campaign version after creating the webhook, then attach the action to its controller. Map the complete partner payload here rather than leaving a generic request body for later:

```bash theme={null}
curl -s -X POST -H "Authorization: Bearer ${TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "quality": "HIGH",
    "enabled": false,
    "webhook_id": "'"${WEBHOOK_ID}"'",
    "data": {
      "person_identifier": "spel@runtime:context.person.email",
      "activity_name": "extole_converted",
      "activity_time": "spel@runtime:context.causeEvent.eventTime"
    }
  }' \
  "${EXTOLE_API_HOST}/v2/campaigns/${CAMPAIGN_ID}/version/${CAMPAIGN_VERSION}/controllers/${CONTROLLER_ID}/actions/webhooks"
```

Keep the controller, action, and webhook disabled until the credential, request handler, payload mapping, and response handling are complete.

### Complete the Request and Response Handlers

The default request handler serializes the webhook action's data as a JSON body, uses the configured method and URL, and applies the client key's default authentication. A `PASSWORD` key adds an `Authorization: Bearer` header.

Write a custom `request` expression when the partner requires a distinct authentication header, body envelope, URL, method, or idempotency key. Build the request from the webhook action data and the client key, following the contract on the partner page. Do not use `createRequestBuilderWithDefaults()` when its default authentication differs from the partner's requirement.

The response handler returns:

* `OK` when processing is complete
* `RETRY` when Extole should use the configured retry intervals

Without a response handler, Extole retries non-2xx responses. Add one when the partner contract distinguishes retryable responses such as `429` or `5xx` from terminal `4xx` configuration or payload errors. Log the terminal status and the minimum response detail needed to diagnose the rejection; do not log credentials or the full participant payload.

A webhook is ready when its request handler produces the partner's required body and authentication and its response handler follows the partner's retry contract. Keep it disabled until both are verified.

### 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 installed component is the wrong source | The listing returns the maintained source plus copies installed in client accounts, in no reliable order. | Choose the Extole-owned source by owner and type. |
| 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. |
| An integration source has a current type but no controllers or webhooks | The source is partner metadata, not a complete outbound implementation. | Build the controller-backed path. |
| The partner rejects authentication | The default key processor uses a header the partner does not accept. | Build the request explicitly with the partner's required authentication header. |

## 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.
* Every requested activity has the matching controller trigger: `SHAREABLE`, `STEP`, or a `FULFILLED` reward event.
* Every controller has a webhook action with the complete partner payload.
* Each webhook exists with its tags and its resolved URL.
* Each `WEBHOOK_ID` setting resolves to a webhook identifier rather than `null`.
* Request and response handlers match the partner's authentication, body, success, and retry contracts.
* `enabled` is `true` on every webhook whose real URL and credential are both configured, and `false` on the rest.

Trigger each Extole event the integration forwards and confirm the partner endpoint receives the expected payload. Enable only the paths whose destination and credential are ready.

## 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)


## Related topics

- [Build a Partner Integration with the Management API](/technical/building-partner-integrations/integration-lifecycle/management-api-integration.md)
- [Integration Categories](/technical/building-partner-integrations/integration-types/integration-categories.md)
- [Sending Partner Events to Extole](/technical/building-partner-integrations/integration-types/partner-events-to-extole.md)
- [Build a Reward Fulfillment Integration](/technical/building-partner-integrations/integration-types/integration-build-reward-fulfillment.md)
- [Validate and Publish an Integration](/technical/building-partner-integrations/integration-lifecycle/integration-validation.md)


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