← back to Designer Wallcoverings

mailers/cc-api/README.md

114 lines

# China Seas → Constant Contact v3 integration

A clean, double-gated Constant Contact **v3 REST API** client for pushing the
China Seas launch mailer into Constant Contact as a custom-code campaign.

**Nothing here calls the live API by default.** Outward writes (campaign create,
schedule) require BOTH `CC_LIVE=1` in the env AND an explicit `--confirm` flag.
The actual send stays gated on Steve's go + compliance sign-off.

Files:
- `cc-client.js` — OAuth2 refresh + `listContactLists` / `createCustomCodeCampaign` / `scheduleCampaign`
- `build-china-seas-campaign.js` — loads `../china-seas-launch.html`, builds the exact payload, validates
- `.cc-token-cache.json` — runtime access-token cache (gitignored)

---

## (a) Create a v3 app → get CLIENT_ID / CLIENT_SECRET

1. Go to <https://developer.constantcontact.com/> and sign in with the Constant
   Contact account that owns the DW contact lists.
2. **My Applications → New Application.**
3. Set the **OAuth2 redirect URI** to a value you control, e.g.
   `https://designerwallcoverings.com/cc-oauth/callback` (or `http://localhost:8080/callback`
   for a one-time local mint). **Steve must supply / approve the redirect URI — do not invent one.**
4. Note the generated **API Key** (`CC_CLIENT_ID`) and **App Secret**
   (`CC_CLIENT_SECRET`). The secret is shown once — copy it immediately.

## (b) Mint the refresh token (Auth-Code + PKCE flow)

Constant Contact v3 uses OAuth2 authorization-code. To get a long-lived refresh
token you request the `offline_access` scope.

1. **Authorize URL** — open this in a browser (replace `CLIENT_ID` and the
   redirect to match your app; `state` is any random string):

   ```
   https://authz.constantcontact.com/oauth2/default/v1/authorize
     ?client_id=CLIENT_ID
     &redirect_uri=REDIRECT_URI
     &response_type=code
     &scope=campaign_data+offline_access
     &state=dw-china-seas
   ```

   (PKCE: optionally add `&code_challenge=...&code_challenge_method=S256` and
   keep the matching `code_verifier` for the token exchange.)

2. Approve. The browser redirects to `REDIRECT_URI?code=AUTH_CODE&state=...`.
   Copy `AUTH_CODE`.

3. **Exchange the code for tokens** (token endpoint
   `https://authz.constantcontact.com/oauth2/default/v1/token`):

   ```sh
   BASIC=$(printf '%s' "CLIENT_ID:CLIENT_SECRET" | base64)
   curl -s -X POST https://authz.constantcontact.com/oauth2/default/v1/token \
     -H "Authorization: Basic $BASIC" \
     -H "Content-Type: application/x-www-form-urlencoded" \
     -d "grant_type=authorization_code" \
     -d "code=AUTH_CODE" \
     -d "redirect_uri=REDIRECT_URI"
     # if you used PKCE, also: -d "code_verifier=YOUR_VERIFIER"
   ```

   The response contains `access_token` (≈2 h) and a `refresh_token`. Keep the
   **`refresh_token`** — that is `CC_REFRESH_TOKEN`. `cc-client.js` uses it to
   mint fresh access tokens automatically (refresh-token grant) and caches the
   access token in `.cc-token-cache.json`.

## (c) Where the creds go — route via the `secrets` skill

Do **not** hardcode. Hand the three values to Steve / paste them and let the
`secrets` skill fan them out (master `.env`, this project, MCP env, etc.):

```
CC_CLIENT_ID=<api key>
CC_CLIENT_SECRET=<app secret>
CC_REFRESH_TOKEN=<refresh token from step b>
```

The secrets skill will add a `routes.json` entry if one doesn't exist yet.

## (d) Run it — dry-run, then the gated confirm

```sh
cd mailers/cc-api

# 1. DRY-RUN (default) — prints payload + validation checklist, NO network.
node build-china-seas-campaign.js --dry-run
# (today this exits non-zero: the 6 vendor logos aren't hosted at https yet —
#  that FAIL is expected and correct, not a bug.)

# 2. Once creds are routed + logos hosted + address confirmed, list lists:
CC_LIVE=1 node -e "require('./cc-client').listContactLists().then(r=>console.log(JSON.stringify(r,null,2)))"

# 3. GATED create (double-gated: needs CC_LIVE=1 AND --confirm; refuses if
#    validation still has FAILs). Without CC_LIVE=1 it NO-OPs and prints intent.
node build-china-seas-campaign.js --confirm            # NO-OP (env gate closed)
CC_LIVE=1 node build-china-seas-campaign.js --confirm  # real create (Steve-gated)
```

`scheduleCampaign(activityId, scheduledDate)` is the same shape — `CC_LIVE=1` +
`confirm:true` required; `scheduledDate` is ISO-8601 (or `"0"` for immediate per CC v3).

---

## Hard gates (do not cross without Steve)

- No live campaign create / schedule / send without Steve's explicit go.
- Sending to a list is an outbound comm → **`vp-compliance-policy` sign-off
  required** (CAN-SPAM / §17529.5 / TCPA / CCPA) before any send.
- See `~/.claude/yolo-queue/pending-approval/china-seas-cc-api-readiness.md`
  for the full blocker checklist.