# Adobe Campaign Transactional Messaging

Use this option when your organization runs **Adobe Campaign** and already uses **Transactional Messaging** for production sends.

Before you go further, make sure you understand how the script provider works in The Wallet Crew. Start with [Adobe Marketing Cloud](https://docs.thewalletcrew.io/connect/email-provider/adobe-marketing-cloud) and the prerequisite [EmailSender extensibility](https://docs.thewalletcrew.io/connect/custom-connector/emailsender-extensibility).

### When to use this

This is a good fit when you already have Adobe Campaign governance, reporting, and pre-existing transactional events.

### Setup required in Adobe

Create and publish a **Transactional Message Event**.

Then decide whether Adobe Campaign will use the rendered HTML from The Wallet Crew or build the final email using Adobe Campaign templates and variables.

You also need a secure server-side authentication method supported by your Adobe Campaign deployment. This is often based on Adobe I/O or IMS.

### Implementation notes (event call)

Your script usually renders the email (if The Wallet Crew controls HTML), then calls the transactional event endpoint with the recipient and context.

Store these values as secrets before you test:

* `adobe-clientId`
* `adobe-clientSecret`
* `adobe-campaign-transactionalUrl` (full Transactional Message Event URL)
* `adobe-orgId` (optional, depends on your Campaign setup)
* `adobe-scope` (optional, depends on your Adobe setup)

{% code title="Adobe Campaign example (SendEmail implementation)" %}

```javascript
import { getSecret } from "neo/secrets";

const CUSTOM_DOMAIN = "wallet.brand.com";
const TENANTID = "brand";

// Adobe IMS (OAuth2 client credentials)
const IMS_TOKEN_URL = "https://ims-na1.adobelogin.com/ims/token/v3";

async function getOptionalSecret(name, fallback) {
  try {
    return (await getSecret(name)) || fallback;
  } catch (e) {
    return fallback;
  }
}

/**
 * @typedef {Object} EmailData
 * @property {string} Subject
 * @property {string} Body
 */

async function sendEmail(recipient, emailTemplate, data, cultures, buildEmail) {
  const email = recipient;
  const countryCode = data["country"] || "fr";
  const language = `${cultures[0]?.toLowerCase()?.substring(0, 2) || "fr"}_${countryCode.toLowerCase()}`;
  const customerId = data["id.customerId"];

  if (!customerId) {
    throw new Error('Missing "id.customerId" in email data.');
  }

  const url = `https://${CUSTOM_DOMAIN}/${TENANTID}/pass?id.customerId=${encodeURIComponent(customerId)}`;

  // Keep Wallet Crew rendering available.
  // If Adobe Campaign owns the template, you can remove this and only send variables.
  const rendered = await buildEmail(emailTemplate);

  const transactionalUrl = await getSecret("adobe-campaign-transactionalUrl");

  const clientId = await getSecret("adobe-clientId");
  const clientSecret = await getSecret("adobe-clientSecret");
  const scope = await getOptionalSecret("adobe-scope", "openid,AdobeID");

  // Optional. Some setups require org id headers, others do not.
  const orgId = await getOptionalSecret("adobe-orgId", null);

  const headers = {
    "x-api-key": clientId,
    "Content-Type": "application/json",
  };
  if (orgId) headers["x-gw-ims-org-id"] = orgId;

  const result = await fetch(transactionalUrl, {
    Method: "POST",
    Authentication: {
      mode: "OAuth2.0",
      grantType: "clientCredentials",
      sendCredentialsAsFormParams: true,
      accessTokenUrl: IMS_TOKEN_URL,
      clientId,
      clientSecret,
      scope,
    },
    Headers: headers,
    Body: JSON.stringify({
      // Payload must match your Transactional Message Event definition.
      email,
      ctx: {
        url,
        language,

        // Optional if you want Campaign to use Wallet Crew rendering:
        subject: rendered.Subject,
        body: rendered.Body,

        // Useful metadata:
        template: emailTemplate,
        customerId,
      },
    }),
    ThrowOnError: false,
  });

  if (result.StatusCode < 200 || result.StatusCode >= 300) {
    throw new Error(`Adobe Campaign call failed (${result.StatusCode}): ${result.ResponseText}`);
  }
}

export default function (context) {
  context.register("runtime.scriptable.emailEngine", {
    SendEmail: sendEmail,
  });
}
```

{% endcode %}

### What to validate

Trigger a real transactional email, then validate these points:

* Your Transactional Message Event is published and callable.
* The payload schema matches the event definition.
* Adobe Campaign accepts the request and processes the message.

### FAQ

<details>

<summary><strong>Do we need a Transactional Message Event?</strong></summary>

Yes. Your script must call a published Transactional Messaging Event endpoint. That event defines the expected payload schema and what variables are available in your Adobe Campaign template.

</details>

<details>

<summary><strong>Can Adobe Campaign render the email instead of The Wallet Crew?</strong></summary>

Yes. In that setup, your script sends only the variables (for example, the pass download URL and customer metadata). Adobe Campaign owns the HTML, subject, and translations.

</details>

<details>

<summary><strong>What authentication method should we use?</strong></summary>

It depends on your Adobe Campaign deployment and how your Adobe organization manages server-to-server access. This page shows an IMS-based approach because it is common, but you should align the final auth method with your Adobe admin team and Adobe’s official documentation for your environment.

</details>

<details>

<summary><strong>What’s the most common reason for request rejection?</strong></summary>

Schema mismatch. The event definition is strict. If your payload fields don’t match the event’s expected structure, Campaign will reject or ignore fields, and templates will render empty values.

</details>
