# Private Offer Field Mapping

Define how data from your CRM (Salesforce or HubSpot) populates the fields in a cloud marketplace private offer.

---

## Overview

[Private Offer Field Mapping](https://console.suger.io/settings?tab=private_offer) controls how the final legal and financial terms of a deal — such as contract value, payment terms, and EULAs — are transmitted from your CRM to AWS, Azure, GCP, or Snowflake when a private offer is created. While co-sell mapping focuses on deal registration, private offer mapping ensures the offer's terms are populated correctly and offer creation does not fail.

## Set Up Private Offer Settings

### Step 1: Access Mapping Settings

1. Log in to the **Fours Console**.
2. Navigate to [**Settings** > **Private Offer**](https://console.suger.io/settings?tab=private_offer).

   ![Private Offer settings tab in Fours Console](images/configure-private-offer-field-mappings-1.png)

3. Locate the section for your CRM (Salesforce or HubSpot).
4. Click **+ New Config** in that section.
5. In the **Create Private Offer Config Info** modal:
   - **Cloud Partner** — Select the hyperscaler (AWS, Azure, GCP, or Snowflake) you are configuring.
   - **Field Mappings** — Map individual fields between the offer and your CRM (detailed in the next step).
   - **Scripts** — (Optional) Provide custom scripts to fill fields that cannot be handled by standard mapping.

     ![Create Private Offer Config Info modal with field mappings and scripts](images/configure-private-offer-field-mappings-2.png)

6. Click **Save**.

### Step 2: Define Outbound Mappings (CRM → Offer)

Cloud partners have rigid data requirements, so you must map your internal CRM fields to the corresponding marketplace schema. Fours supports these types of field mappings:

- **Static Values (Default Value)** — Use for fields that are mandatory but identical for every offer, such as your standard **Support Terms** or a specific **Seller Note**.
- **Direct Mapping** — Link fields 1:1.
  - _Salesforce example_: Map `Opportunity.Amount` → `Offer.Amount`.
  - _HubSpot example_: Map `Deal.Amount` → `Offer.Total_Value`.
- **Expression Mode (Advanced)** — Use logic for data transformations.
  - _Example_: Calculate a custom discount: `Opportunity.Amount * (1 - Opportunity.Discount_Pct__c)`.
  - _Example_: Concatenate a unique offer name: `Account.Name + " - Private Offer - " + Today()`.

#### Key Fields to Map

Ensure these high-impact fields are correctly mapped to prevent offer creation failures.

**AWS private offer**

| Field Name | Source Type | Operational Purpose |
| --- | --- | --- |
| Salesforce Opportunity ID | Salesforce Field (`Id`) | Links the private offer to the correct CRM record for tracking. |
| Product ID | Salesforce Field | Maps the Fours product to the specific AWS listing ID. |
| Offer Name | Expression | Defines the offer title. Example: `{{ .Name }} - {{ .Account.Name }}`. |
| Buyer AWS Account ID | Salesforce Field | Required for AWS to authorize the specific buyer to view the offer. |
| Contacts | Field / Manual | Defines who receives notification emails when the offer is live. |
| EULA Type | Salesforce Field | Specifies the legal terms (Standard AWS Terms vs. Custom EULA). |
| Offer Expiry Date | Salesforce Field | The "Accept By" date (format: `YYYY-MM-DD`). |
| Start / End Date | Salesforce Field | Defines the contract term duration. |

**Azure private offer**

Azure mappings focus on identifying the correct billing account and ensuring offer dates align with Microsoft's UTC requirements.

| Field Name | Source Type | Operational Purpose |
| --- | --- | --- |
| Salesforce Opportunity Id | Salesforce Field | Identifies the CRM record for cross-platform tracking. |
| Product Id | Salesforce Field | Links the offer to the specific Azure Marketplace listing. |
| Offer Name | Expression | Custom title for the offer (e.g., `{{ .Name }}`). |
| Buyer Azure Billing Account ID | Salesforce Field | Authorizes the specific Azure account to accept the offer. |
| Prepared By Email | Salesforce Field | Identifies the person responsible for offer creation. |
| Offer Expiry Date | Salesforce Field | The acceptance deadline (format: `YYYY-MM-DD`). |
| Offer End Date | Salesforce Field | The official termination date of the contract. |
| Start Date | Salesforce Field | The service commencement date (default: acceptance date). |

**GCP private offer**

GCP requires more granular contact information than other partners to keep both the buyer and seller teams informed throughout the procurement lifecycle.

:::tip
For **GCP Billing IDs**, add a validation rule in Salesforce so the string follows the hex format `XXXXXX-XXXXXX-XXXXXX`. If this ID is off by a single digit, the GCP API call fails with a vague "Validation Error."
:::

| Field Name | Source Type | Operational Purpose |
| --- | --- | --- |
| Buyer GCP Billing Account ID | Salesforce Field | Must follow the `000000-000000-000000` format. |
| Buyer Organization Name | Salesforce Field | The legal entity name of the buyer being notified. |
| Buyer Contact Email | Salesforce Field | The specific buyer address notified by GCP to accept the offer. |
| Sales Contact Email | Salesforce Field | Your team's contact for GCP-led communications. |
| Term Length in Months | Salesforce Field | Determines the contract duration (range: 1 to 60 months). |
| Offer Expiry Date | Salesforce Field | The UTC date before which the offer must be accepted. |

**Snowflake private offer**

Snowflake mappings introduce the **Offer Display Name**, designed for customer-facing clarity within the Snowflake Marketplace UI.

| Field Name | Source Type | Operational Purpose |
| --- | --- | --- |
| Offer Display Name | Salesforce Field | The customer-facing title shown in the Snowflake Marketplace. |
| Offer Expiry Date | Salesforce Field | The date the offer becomes invalid if not accepted. |
| Start Date | Salesforce Field | When the Snowflake service entitlements begin. |
| Offer End Date | Salesforce Field | The final day of the service term. |

**Offer names are cleaned for the target marketplace.** When a field mapping fills an offer name, Fours replaces each character that marketplace doesn't accept with a space, then collapses repeated spaces and trims the ends.

| Marketplace | Characters replaced with a space |
| --- | --- |
| AWS | `;` `"` `'` `<` `>` `\` |
| Azure | Everything except `A`–`Z`, `a`–`z`, `0`–`9`, spaces, `_`, and `-` |
| Snowflake | Everything except `A`–`Z`, `a`–`z`, `0`–`9`, spaces, and `_` |
| GCP | Nothing |

### Step 3: Set Listing Filters (Optional)

On the main **Private Offer** settings page, you can enable a **List Products Filter**. This lets you write a custom script that restricts which marketplace products your sales team can choose from when creating an offer, based on specific customer needs.

![List Products Filter script on the Private Offer settings page](images/configure-private-offer-field-mappings-3.png)

#### Test the filter

Click **Test**, next to **Save**, to run the script in the editor — saved or not — against your organization's first 1,000 products. Nothing is saved.

- If the script depends on the CRM record an offer is created from, enter a **Salesforce Opportunity ID**, **Salesforce Quote ID**, **HubSpot Deal ID**, or **Dynamics 365 Opportunity ID**, then click **Run again**.
- The result lists the products the script selects from those 1,000. The test never falls back to listing everything: *The script matched no products* means it selected none — when a rep creates an offer, a script that sets no `$target.productIds` is ignored and every product is listed, while IDs that match no product list nothing.
- **Script error** shows the JavaScript error when the script throws or sets `$target.productIds` to anything other than an array of product ID strings.
- Only organization Admins can run the test; anyone else sees **Not allowed**. A script longer than 12,000 bytes once URL-encoded is too large to test here — save it and try it on a real offer.
- The offer form pages through all your products, so a script that reasons about the whole product set can behave differently there.

### Step 4: Link Offers Back to the Record They Came From (Salesforce, Optional)

The **Salesforce** section of the [**Private Offer**](https://console.suger.io/settings?tab=private_offer) settings page starts with **Offer Created From** — the object your sellers create offers from — and two options that work with it.

**Custom Backfill Line** keeps a link from each Salesforce Offer record to the record it was created from. Turn it on and pick the lookup on `Suger__Offer__c` that should hold the link:

- Lookups that point at the Offer Created From object are listed first and tagged **Recommended**. For Quote, Opportunity and Account, turning the switch on preselects the field Fours already writes for that object (`Suger__QuoteLookup__c`, `Suger__Opportunity__c` or `Suger__Account__c`).
- A field tagged **No edit access** can't be selected: the lookup must already exist on `Suger__Offer__c`, with edit access granted to the Fours integration user — Fours cannot create it or grant itself the permission. A field tagged **Fours-owned** is one Fours overwrites.
- **Refresh Schema** reloads the fields on `Suger__Offer__c`. Click it after you add a lookup in Salesforce, or the list can be up to seven days old.
- Mappings saved for other objects are listed under **Also mapped on this org.** with a remove icon. Turning the switch off, or removing a mapping, asks **Remove this mapping?** first; links already written on Offer records stay as they are.

When a seller then creates an offer in the Salesforce app from a record of that object, the app writes the record's ID into the lookup, using the seller's own Salesforce permissions. On **Create** and **Save as Draft**, a failed write doesn't stop the offer — it is saved without the link. On **Submit for Approval**, a failed write is reported as an error.

**Filter Condition** narrows the list of related records a seller can pick when they start an offer from an Opportunity and your Fours Salesforce settings name an **Additional Triggering Object**. Leave it empty to list every record. Click **Edit condition** to open **Triggering Object Filter Condition**, click **Peek** to see how many records of the object match across your org, then click **Done** — and **Save** the Salesforce section to store it.

The same two settings also appear on the Salesforce integration's edit form, as **Link Offer to Triggering Object** and **Triggering Object Filter Condition**; both places change the same stored values. Saving either needs the Admin role.

```d2
direction: right
seller: "Seller creates an offer\nin the Salesforce app"
record: "Record of the\nOffer Created From object"
offer: "Salesforce Offer record\n(Suger__Offer__c)"
seller -> record: "starts from"
seller -> offer: "creates"
offer -> record: "Custom Backfill Line\nlookup holds its ID"
```

### Step 5: Choose a EULA Document Source (Salesforce, Optional)

When a seller creates a private offer in the [Fours Salesforce app](/salesforce-app/salesforce-app-offer/#attach-your-own-eula-files), the EULA section lets them attach PDFs from the Opportunity's or the Quote's attachments. If your signed agreements are stored on a different record — for example, an `Agreement__c` record that each Quote looks up to — select that lookup here, and sellers can attach PDFs from the related record's Salesforce Files too.

1. On the [**Private Offer**](https://console.suger.io/settings?tab=private_offer) settings page, go to the **Salesforce** section.
2. Confirm **Offer Created From** — *the Salesforce object a private offer is created from*: a Quote, an Opportunity, a CPQ quote, or any custom object. The lookup list comes from this object, and sellers see the new button only when they launch an offer from a record of this object. It defaults to `Opportunity`, and it is also the object your field mappings read from.
3. In **EULA Document Source**, select the lookup field that points to the record holding the files. Each option shows the field label, its relationship name, and the object it points to — for example, `Agreement (Agreement__r) → Agreement__c` — and **Related object** confirms the target once you select one.
4. Click **Save** in the **Salesforce** section.

```d2
direction: right
console: "Fours Console\nEULA Document Source" { style.stroke-dash: 4 }
launch: "Record the offer is launched from\n(Offer Created From, e.g. Quote)"
related: "Related record\n(e.g. Agreement)"
files: "Salesforce Files\non the related record"
offer: "EULA on the\nprivate offer"
console -> launch: "names one lookup on" { style.stroke-dash: 4 }
launch -> related: "lookup"
related -> files
files -> offer: "seller picks PDFs"
```

Sellers who launch an offer from a record of that object then see a button named after the lookup — **Select from Agreement files**, for example — in the offer form's EULA section, alongside the Opportunity or Quote attachment buttons available there. It lists the Salesforce Files on the related record, and the seller chooses which PDFs to attach — nothing is attached automatically.

- **One direct lookup.** Only lookup fields on the Offer Created From object that point to a single object are listed; lookups to User and polymorphic lookups are left out. If the object has none, the selector says *No supported lookup fields on this object.* A chain such as Opportunity → primary Quote → Agreement is not supported — configure a direct lookup on the object sellers launch from.
- **Changing Offer Created From clears the selection**, so pick a lookup on the new object before you save.
- **Refresh after changing fields in Salesforce.** The refresh icon beside **EULA Document Source** reloads the object's fields.
- **The seller's own Salesforce access applies.** If the lookup is empty on the record, or the seller can't see the related record, the button is disabled and shows *No related record found.*
- **PDFs only, within the form's limit** — 20 MB per file on AWS offer forms, 3.5 MB on Azure and GCP offer forms.
- **To turn it off,** select **None** and save. EULAs already attached to offers are not changed.

### Step 6: Tag Products by Product Line (Optional, BETA)

**Product Tagging** records the product line each product belongs to. With **Contact routing** on, Fours files the buyer contacts on a product's offers under its product line — the Salesforce app groups an account's [Buyer Contacts](/salesforce-app/salesforce-app-page-components/#buyer-contacts) that way — and the Products page shows each product's [tag](/console/products/#product-tags).

```d2
direction: right
product: "Product with\na product line"
offers: "Its private offers"
contacts: "Buyer contacts\nnamed on those offers"
sfdc: "Salesforce app:\nBuyer Contacts grouped\nby product line"
product -> offers
offers -> contacts: "Contact routing on"
contacts -> sfdc
```

1. On the [**Private Offer**](https://console.suger.io/settings?tab=private_offer) settings page, go to **Product Tagging**.
2. Click **New Config** and choose the cloud: **AWS**, **AZURE**, or **GCP**. That cloud's tagging dialog opens.
3. Tag each product: click **+ Tag** on its row, then pick an existing tag or type a new one. To tag several products at once, select them — or **Select all visible** — and use **Add tag to selection**. **Search by product name or ID** and **Untagged only** narrow the list. Click **Save**.
4. Turn on **Contact routing**, then click **Save** in the Product Tagging section.

The **Coverage** column tracks each cloud — for example, *12/40 tagged · 3 tags* — and the pencil icon reopens its tagging dialog.

- **One product line per product**, up to 100 characters. Casing counts: *Security* and *security* are two product lines.
- **Only buyer contacts are filed.** Your own sales reps, your partners' contacts, and anyone at your organization's email domain are left out.
- **Turning on Contact routing** also files the contacts of products tagged while it was off. Turning it off hides the product lines and stops filing contacts.
- **Deleting a cloud's configuration** (trash icon) first removes the tags from each of that cloud's tagged products.
- **Roles.** Tagging products needs the Editor or Admin role — for anyone else the tagging dialog is read-only. Creating or deleting a configuration and saving **Contact routing** need the Admin role.

## Troubleshooting

| Issue | Cause | Resolution |
| --- | --- | --- |
| "Invalid ID" on creation | The CRM field contains a typo or an incorrectly formatted account ID. | Copy-paste the ID directly from the AWS/Azure portal into the CRM record and refresh the creation modal. |
| Pricing logic error | Mathematical errors or missing parenthesis in Expression Mode. | Use the **Test** button in the mapping modal to validate your script before saving. |
| Missing required fields | The mapping points to a CRM field that is currently blank on that record. | Populate the data in your CRM, or add a Default Value in the configuration to act as a fallback. |
| Widget says "Integration Missing" | The CRM integration is disconnected or the API key is invalid. | Verify connection status in **Settings** > **Integrations** and ensure your API client is active. |
| "Field Not Found" error | A CRM field used in a mapping was deleted or renamed. | Update the corresponding **Private Offer Field Mappings** in Fours to reflect the new field names. |
| "Configured lookup … is unavailable" under **EULA Document Source**, and the Salesforce section's **Save** is disabled | The saved lookup field no longer appears in the object's field list — for example, it was deleted or renamed in Salesforce. | Click the refresh button beside the selector, then select another lookup — or **None** — and save. |
| *Custom Backfill Line was not saved: …* after **Save** | Neither the backfill mapping nor the filter condition was stored — for example because the organization has no Salesforce integration. Every other setting in the Salesforce section was saved. | Check the Salesforce integration under **Settings → Integrations**, then save again. |
| *Could not read the fields on Suger__Offer__c: …* | Fours couldn't read the Offer object's fields from Salesforce. | Check the Salesforce connection, then click **Refresh Schema**. |
| No **Select from … files** button on the Salesforce offer form | No EULA Document Source is saved, or the offer was launched from a record that isn't the **Offer Created From** object. | Save a lookup in [Step 5](#step-5-choose-a-eula-document-source-salesforce-optional), and launch the offer from a record of that object. |

## FAQ

**Can I manage contract renewals and upsells from HubSpot or Salesforce?**

Yes. Fours supports [Agreement-Based Offers (ABO)](/console/private-offers/) natively within the Fours widget, allowing sales teams to modify active entitlements directly from the deal record.

**What happens if I try to edit an offer after it has been sent?**

You cannot edit an ongoing offer once it is in a `Pending_Acceptance` state. The best practice is to cancel the existing offer and create a new one with the corrected terms.

**Can I map different pricing models (Flat Fee vs. Consumption) through field mapping?**

Yes. Use **Expression Mode** to dynamically set the pricing model based on a CRM field. For example, if your HubSpot deal includes a custom property for "Billing Type," you can write an expression that instructs Fours to select "Usage-Based" or "Contract" pricing on the marketplace offer automatically.

**Does Fours support mapping for multi-year installment plans?**

Yes. If your CRM (like Salesforce) uses a related list for payment schedules, you can map these to the marketplace's installment fields. This ensures the buyer sees the exact payment dates and amounts in their marketplace console that were agreed upon in your CRM.

**How does Fours handle EULA (End User License Agreement) mapping?**

You have two options:

1. **Static** — Set a **Default Value** with a URL to your standard EULA.
2. **Dynamic** — Map a CRM field (e.g., `Custom_EULA_Link__c`) to the marketplace legal terms field. This is ideal when you negotiate custom legal terms for specific enterprise deals.

To let sellers attach signed PDFs stored on a related Salesforce record instead — an Agreement that a Quote looks up to, for example — set a **EULA Document Source** ([Step 5](#step-5-choose-a-eula-document-source-salesforce-optional)).

**If I change the "Amount" in my CRM, will an already-sent private offer update automatically?**

No. Once a private offer is created and sent to a buyer, its terms are locked by the cloud provider (AWS/Azure/GCP). To change the amount, cancel the existing offer in the Fours Console, update the CRM record, and generate a new offer.

**What is the "Scripts" section in the private offer configuration used for?**

The **Scripts** section allows advanced automation beyond simple 1:1 mapping. You can write custom JavaScript to perform complex lookups or conditional logic — such as applying specific tax codes or partner incentives — before the offer object is finalized.

**Can I restrict which products my team can map to?**

Yes. Use the **List Products Filter** script on the main Private Offer settings page to filter the marketplace catalog available to your sales reps, ensuring they only create offers for approved products or SKUs.

**How do I ensure the "Buyer Identifier" is always accurate?**

Make the **Buyer Account ID** (AWS Account ID or Azure Tenant ID) a required field in your CRM. In Fours, you can use **Expression Mode** to validate the length of the string (e.g., ensuring it is exactly 12 digits for AWS) before allowing the offer to be created.

**Does Fours track who modified the mapping logic?**

Yes. Every configuration has a **History** button showing the previous version of the mapping, the new version, the user who made the change, and the timestamp — useful for troubleshooting pricing mismatches after a logic update.
