diff --git a/email/pardot.go b/email/pardot.go index 1d1c7299a..421bb57b9 100644 --- a/email/pardot.go +++ b/email/pardot.go @@ -18,10 +18,12 @@ const ( // tokenPath is the path to the Salesforce OAuth2 token endpoint. tokenPath = "/services/oauth2/token" - // contactsPath is the path to the Pardot v5 Prospects endpoint. This - // endpoint will create a new Prospect if one does not already exist with - // the same email address. - contactsPath = "/api/v5/objects/prospects" + // contactsPath is the path to the Pardot v5 Prospect upsert-by-email + // endpoint. This endpoint will create a new Prospect if one does not + // already exist with the same email address. + // + // https://developer.salesforce.com/docs/marketing/pardot/guide/prospect-v5.html#prospect-upsert-by-email + contactsPath = "/api/v5/objects/prospects/do/upsertLatestByEmail" // maxAttempts is the maximum number of attempts to retry a request. maxAttempts = 3 @@ -60,7 +62,7 @@ type PardotClientImpl struct { businessUnit string clientId string clientSecret string - contactsURL string + endpointURL string tokenURL string token *oAuthToken clk clock.Clock @@ -70,7 +72,7 @@ var _ PardotClient = &PardotClientImpl{} // NewPardotClientImpl creates a new PardotClientImpl. func NewPardotClientImpl(clk clock.Clock, businessUnit, clientId, clientSecret, oauthbaseURL, pardotBaseURL string) (*PardotClientImpl, error) { - contactsURL, err := url.JoinPath(pardotBaseURL, contactsPath) + endpointURL, err := url.JoinPath(pardotBaseURL, contactsPath) if err != nil { return nil, fmt.Errorf("failed to join contacts path: %w", err) } @@ -83,7 +85,7 @@ func NewPardotClientImpl(clk clock.Clock, businessUnit, clientId, clientSecret, businessUnit: businessUnit, clientId: clientId, clientSecret: clientSecret, - contactsURL: contactsURL, + endpointURL: endpointURL, tokenURL: tokenURL, token: &oAuthToken{}, clk: clk, @@ -140,6 +142,19 @@ func redactEmail(body []byte, email string) string { return string(bytes.ReplaceAll(body, []byte(email), []byte("[REDACTED]"))) } +type prospect struct { + // Email is the email address of the prospect. + Email string `json:"email"` +} + +type upsertPayload struct { + // MatchEmail is the email address to match against existing prospects to + // avoid adding duplicates. + MatchEmail string `json:"matchEmail"` + // Prospect is the prospect data to be upserted. + Prospect prospect `json:"prospect"` +} + // SendContact submits an email to the Pardot Contacts endpoint, retrying up // to 3 times with exponential backoff. func (pc *PardotClientImpl) SendContact(email string) error { @@ -156,7 +171,10 @@ func (pc *PardotClientImpl) SendContact(email string) error { return fmt.Errorf("failed to update token: %w", err) } - payload, err := json.Marshal(map[string]string{"email": email}) + payload, err := json.Marshal(upsertPayload{ + MatchEmail: email, + Prospect: prospect{Email: email}, + }) if err != nil { return fmt.Errorf("failed to marshal payload: %w", err) } @@ -165,7 +183,7 @@ func (pc *PardotClientImpl) SendContact(email string) error { for attempt := range maxAttempts { time.Sleep(core.RetryBackoff(attempt, retryBackoffMin, retryBackoffMax, retryBackoffBase)) - req, err := http.NewRequest("POST", pc.contactsURL, bytes.NewReader(payload)) + req, err := http.NewRequest("POST", pc.endpointURL, bytes.NewReader(payload)) if err != nil { finalErr = fmt.Errorf("failed to create new contact request: %w", err) continue diff --git a/test/pardot-test-srv/main.go b/test/pardot-test-srv/main.go index e247ff345..a40879d42 100644 --- a/test/pardot-test-srv/main.go +++ b/test/pardot-test-srv/main.go @@ -85,7 +85,7 @@ func (ts *testServer) checkToken(w http.ResponseWriter, r *http.Request) { } } -func (ts *testServer) createContactsHandler(w http.ResponseWriter, r *http.Request) { +func (ts *testServer) upsertContactsHandler(w http.ResponseWriter, r *http.Request) { ts.checkToken(w, r) businessUnitId := r.Header.Get("Pardot-Business-Unit-Id") @@ -100,19 +100,22 @@ func (ts *testServer) createContactsHandler(w http.ResponseWriter, r *http.Reque return } - type contactData struct { - Email string `json:"email"` + type upsertPayload struct { + MatchEmail string `json:"matchEmail"` + Prospect struct { + Email string `json:"email"` + } `json:"prospect"` } - var contact contactData - err = json.Unmarshal(body, &contact) + var payload upsertPayload + err = json.Unmarshal(body, &payload) if err != nil { http.Error(w, "Failed to parse request body", http.StatusBadRequest) return } - if contact.Email == "" { - http.Error(w, "Missing 'email' field in request body", http.StatusBadRequest) + if payload.MatchEmail == "" || payload.Prospect.Email == "" { + http.Error(w, "Missing 'matchEmail' or 'prospect.email' in request body", http.StatusBadRequest) return } @@ -122,7 +125,7 @@ func (ts *testServer) createContactsHandler(w http.ResponseWriter, r *http.Reque // with a small number of contacts, so it's fine. ts.contacts.created = ts.contacts.created[1:] } - ts.contacts.created = append(ts.contacts.created, contact.Email) + ts.contacts.created = append(ts.contacts.created, payload.Prospect.Email) ts.contacts.Unlock() w.Header().Set("Content-Type", "application/json") @@ -198,7 +201,7 @@ func main() { // Pardot API Server pardotMux := http.NewServeMux() - pardotMux.HandleFunc("/api/v5/objects/prospects", ts.createContactsHandler) + pardotMux.HandleFunc("/api/v5/objects/prospects/do/upsertLatestByEmail", ts.upsertContactsHandler) pardotMux.HandleFunc("/contacts", ts.queryContactsHandler) pardotServer := &http.Server{