Marketplace Offers — Onboarding
The Marketplace connector setup creates a cross-account IAM role in the AWS account that holds your Marketplace seller registration. FlowState assumes this role (with ExternalId protection) to call the AWS Marketplace Catalog API on your behalf. No long-lived AWS credentials are stored in FlowState.
There are three equivalent paths to create the role — OpenTofu, CloudFormation, or the IAM console directly. All three produce the same role, the same trust policy, and the same inline permissions policy. Choose the path that matches your team’s tooling.
Pre-step (common to all paths)
Before creating the role, make sure the marketplace_offers feature is enabled for your
tenant (Settings → Features — see Prerequisites). Once it is, call
GET /v1/tenants/{id}/integrations/marketplace (or the get_marketplace_connector_status MCP
tool). Before a connector exists, this returns:
{ "connected": false, "external_id": "<a fresh, tenant-unique secret>" }FlowState generates this ExternalId the first time it’s asked for, and reuses the same one on every subsequent call until you disconnect. Copy it — you’ll need it in all three paths below.
You’ll also need:
| Value | Where it comes from |
|---|---|
| ExternalId | GET /v1/tenants/{id}/integrations/marketplace, as above. |
| FlowState principal ARNs | Two roles, both required: arn:aws:iam::<FLOWSTATE_AWS_ACCOUNT>:role/flowstate-<stage>-backend (REST API — the web app and the HubSpot card) and arn:aws:iam::<FLOWSTATE_AWS_ACCOUNT>:role/flowstate-<stage>-mcp-execution (MCP tools). Listing only the first works through the app and the card, then fails with AccessDenied through any MCP client — see Trust policy. Ask your FlowState contact for the AWS account id and stage (dev/prod) your tenant is connected to. |
| Region | us-east-1 — the AWS Marketplace Catalog API is us-east-1 only. |
Tab 1 — OpenTofu
Best for: teams that manage infrastructure as code and want to version-control the connector role alongside their other AWS resources.
Create a fresh directory in the AWS account that holds your Marketplace seller registration, containing four files:
# versions.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
}# variables.tf
variable "flowstate_principal_arns" {
description = "List of FlowState IAM role ARNs (backend) that may assume this role. Always paste the current list from FlowState — it grows as FlowState adds services."
type = list(string)
validation {
condition = length(var.flowstate_principal_arns) > 0 && !contains(var.flowstate_principal_arns, "undefined")
error_message = "flowstate_principal_arns must contain at least one ARN. Regenerate the snippet from FlowState."
}
}
variable "external_id" {
description = "Per-tenant external ID for sts:AssumeRole. Prevents confused-deputy attacks."
type = string
}# main.tf
resource "aws_iam_role" "flowstate_marketplace_connector" {
name = "FlowStateMarketplaceConnector"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { AWS = var.flowstate_principal_arns }
Action = ["sts:AssumeRole"]
Condition = {
StringEquals = { "sts:ExternalId" = var.external_id }
}
}]
})
}
resource "aws_iam_role_policy" "flowstate_marketplace_connector" {
name = "FlowStateMarketplaceCatalogAccess"
role = aws_iam_role.flowstate_marketplace_connector.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "MarketplaceCatalogList"
Effect = "Allow"
Action = ["aws-marketplace:ListEntities"]
Resource = "*"
},
{
Sid = "MarketplaceCatalogManage"
Effect = "Allow"
Action = [
"aws-marketplace:DescribeEntity",
"aws-marketplace:StartChangeSet",
"aws-marketplace:DescribeChangeSet",
]
Resource = [
"arn:${data.aws_partition.current.partition}:aws-marketplace:us-east-1:${data.aws_caller_identity.current.account_id}:AWSMarketplace/SaaSProduct/*",
"arn:${data.aws_partition.current.partition}:aws-marketplace:us-east-1:${data.aws_caller_identity.current.account_id}:AWSMarketplace/Offer/*",
"arn:${data.aws_partition.current.partition}:aws-marketplace:us-east-1:${data.aws_caller_identity.current.account_id}:AWSMarketplace/ChangeSet/*",
"arn:${data.aws_partition.current.partition}:aws-marketplace:us-east-1:${data.aws_caller_identity.current.account_id}:AWSMarketplace/Seller/*",
]
},
{
Sid = "MarketplaceAgreementRead"
Effect = "Allow"
Action = ["aws-marketplace:SearchAgreements"]
Resource = "*"
Condition = {
"ForAllValues:StringEquals" = {
"aws-marketplace:AgreementType" = ["PurchaseAgreement"]
}
StringEquals = {
"aws-marketplace:PartyType" = "Proposer"
}
}
},
]
})
}
data "aws_caller_identity" "current" {}
data "aws_partition" "current" {}# outputs.tf
output "role_arn" {
description = "ARN of the FlowStateMarketplaceConnector role."
value = aws_iam_role.flowstate_marketplace_connector.arn
}See Trust policy for exactly what each statement does. Run, from that directory:
tofu init
tofu apply \
-var 'flowstate_principal_arns=["<backend ARN from the pre-step>","<mcp-execution ARN from the pre-step>"]' \
-var external_id=<ExternalId from the pre-step>Make sure your AWS provider/credentials target us-east-1 and the account that holds your
Marketplace seller registration before applying. After tofu apply completes, copy the
role_arn output value.
Tab 2 — CloudFormation
Best for: teams that prefer one-click deployment or are not familiar with Terraform/OpenTofu.
AWSTemplateFormatVersion: "2010-09-09"
Description: >
FlowState Marketplace Connector — creates the IAM role that grants FlowState
read/write access to your AWS Marketplace Catalog (SaaS products and
private offers), so FlowState can create and manage Marketplace private
offers on your behalf.
Parameters:
FlowstatePrincipalArns:
Type: CommaDelimitedList
Description: >
Comma-separated list of FlowState IAM role ARNs that may assume this role
(backend). Provided by FlowState during onboarding — always paste the
current value from FlowState; the list grows as FlowState adds services.
ExternalId:
Type: String
Description: >
Per-tenant external ID for sts:AssumeRole (provided by FlowState
during onboarding). Prevents confused-deputy attacks.
Resources:
FlowStateMarketplaceConnectorRole:
Type: AWS::IAM::Role
Properties:
RoleName: FlowStateMarketplaceConnector
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
AWS: !Ref FlowstatePrincipalArns
Action:
- sts:AssumeRole
Condition:
StringEquals:
sts:ExternalId: !Ref ExternalId
Policies:
- PolicyName: FlowStateMarketplaceCatalogAccess
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: MarketplaceCatalogList
Effect: Allow
Action:
- aws-marketplace:ListEntities
Resource: "*"
- Sid: MarketplaceCatalogManage
Effect: Allow
Action:
- aws-marketplace:DescribeEntity
- aws-marketplace:StartChangeSet
- aws-marketplace:DescribeChangeSet
Resource:
- !Sub "arn:${AWS::Partition}:aws-marketplace:us-east-1:${AWS::AccountId}:AWSMarketplace/SaaSProduct/*"
- !Sub "arn:${AWS::Partition}:aws-marketplace:us-east-1:${AWS::AccountId}:AWSMarketplace/Offer/*"
- !Sub "arn:${AWS::Partition}:aws-marketplace:us-east-1:${AWS::AccountId}:AWSMarketplace/ChangeSet/*"
- !Sub "arn:${AWS::Partition}:aws-marketplace:us-east-1:${AWS::AccountId}:AWSMarketplace/Seller/*"
- Sid: MarketplaceAgreementRead
Effect: Allow
Action:
- aws-marketplace:SearchAgreements
Resource: "*"
Condition:
ForAllValues:StringEquals:
aws-marketplace:AgreementType:
- PurchaseAgreement
StringEquals:
aws-marketplace:PartyType: Proposer
Outputs:
RoleArn:
Description: ARN of the FlowStateMarketplaceConnector role.
Value: !GetAtt FlowStateMarketplaceConnectorRole.Arn- In the AWS account that holds your Marketplace seller registration, switch region to N. Virginia (us-east-1).
- Open CloudFormation → Create stack, upload the template above.
- Set
FlowstatePrincipalArnsto both FlowState principal ARNs from the pre-step, andExternalIdto the ExternalId from the pre-step. - Check I acknowledge that AWS CloudFormation might create IAM resources with custom names.
- Click Create stack and wait for
CREATE_COMPLETE. - Open the Outputs tab and copy the
RoleArnvalue.
Tab 3 — Manual (IAM Console)
Best for: teams that prefer step-by-step control via the AWS Console UI, or where IaC tooling is not available.
-
Sign in to the AWS account that holds your Marketplace seller registration.
-
Switch region to N. Virginia (us-east-1) using the region selector in the top-right corner. (IAM itself is global, but keep this region selected while you work — the resource ARNs in the permissions policy below are pinned to
us-east-1.) -
Open IAM → Roles → Create role.
-
On the Trusted entity type screen, select AWS account, then Another AWS account.
-
In the Account ID field, paste the AWS account id from the FlowState principal ARN in the pre-step.
-
Check Require external ID and paste the ExternalId from the pre-step.
-
Click Next, then Next again (skip the managed-policy search — this role uses an inline policy, added after the role is created).
-
Set the role name to exactly
FlowStateMarketplaceConnector. -
Click Create role.
-
Open the newly created role. Under Permissions, click Add permissions → Create inline policy, switch to the JSON editor, and paste:
{ "Version": "2012-10-17", "Statement": [ { "Sid": "MarketplaceCatalogList", "Effect": "Allow", "Action": ["aws-marketplace:ListEntities"], "Resource": "*" }, { "Sid": "MarketplaceCatalogManage", "Effect": "Allow", "Action": [ "aws-marketplace:DescribeEntity", "aws-marketplace:StartChangeSet", "aws-marketplace:DescribeChangeSet" ], "Resource": [ "arn:aws:aws-marketplace:us-east-1:<YOUR_ACCOUNT_ID>:AWSMarketplace/SaaSProduct/*", "arn:aws:aws-marketplace:us-east-1:<YOUR_ACCOUNT_ID>:AWSMarketplace/Offer/*", "arn:aws:aws-marketplace:us-east-1:<YOUR_ACCOUNT_ID>:AWSMarketplace/ChangeSet/*", "arn:aws:aws-marketplace:us-east-1:<YOUR_ACCOUNT_ID>:AWSMarketplace/Seller/*" ] }, { "Sid": "MarketplaceAgreementRead", "Effect": "Allow", "Action": ["aws-marketplace:SearchAgreements"], "Resource": "*", "Condition": { "ForAllValues:StringEquals": { "aws-marketplace:AgreementType": ["PurchaseAgreement"] }, "StringEquals": { "aws-marketplace:PartyType": "Proposer" } } } ] }Replace
<YOUR_ACCOUNT_ID>with your own AWS account id, then name and save the policy. -
In the Trust relationships tab, confirm the trust policy matches Trust policy —
Principal.AWSmust list both FlowState principal ARNs from the pre-step, conditioned on your ExternalId. Listing only the-backendone works through the web app and the HubSpot card, then fails withAccessDeniedthrough every MCP client. -
Copy the role’s ARN from the Summary panel (format:
arn:aws:iam::<your-account-id>:role/FlowStateMarketplaceConnector).
Connecting the role to FlowState
However you created it, connecting is one call:
POST /v1/tenants/{id}/integrations/marketplace
{ "role_arn": "<the ARN you copied above>" }or the connect_marketplace_connector(role_arn) MCP tool. FlowState assumes the role with your
tenant’s ExternalId, then makes one real, cheap read call
(aws-marketplace:ListEntities for SaaS products) to confirm the role is not just assumable but
actually permissioned. On success, it stores the connector and returns the AWS account id it
discovered:
{ "status": "connected", "account_id": "<your AWS account id>" }If verification fails, the response names what failed and, for an AssumeRole denial specifically, a hint pointing at the trust policy and ExternalId. See Troubleshooting.
Done
Once connected, the list_marketplace_products MCP tool (or GET /v1/tenants/{id}/integrations/marketplace/products) should return your seller account’s SaaS
products. See Overview for the full action list, and for where offer work
actually happens today.
Migration — the connector policy gained SearchAgreements and the Seller entity
Applies to: every tenant whose Marketplace connector stack was deployed before 2026-08-31.
What changed
The connector role’s permission policy gained a fifth action, aws-marketplace:SearchAgreements,
in a new MarketplaceAgreementRead statement.
| Action | Service | Purpose |
|---|---|---|
ListEntities | marketplace-catalog | Find your SaaS products and offers |
DescribeEntity | marketplace-catalog | Read one product or offer |
StartChangeSet | marketplace-catalog | Create, clone, release and expire offers |
DescribeChangeSet | marketplace-catalog | Poll a submitted change |
SearchAgreements | marketplace-agreement | New. List the purchase agreements your offers have produced — this is what the Agreements view reads. |
DescribeEntity on Seller/* | marketplace-catalog | New. Reads your seller profile’s DisbursementPreferences to learn which currencies your account has provisioned — AWS only allows an offer in a currency you can be disbursed in, so the offer form offers exactly those and nothing else. Read-only; no write is granted on the seller profile. |
Nothing was removed. The four catalog actions keep exactly the access they had.
Who must act
Any tenant who deployed the connector stack before the date above. Without the Seller
scope the offer form cannot tell which currencies you may use. Everything else keeps
working: products, offers, create, clone, release and expire are all marketplace-catalog and are
unaffected. Only the agreements list fails, and it fails as a 500, not as a visible permission
error — the underlying AccessDeniedException is logged server-side and the API returns a generic
INTERNAL_ERROR. Tenants who onboard after that date get the full policy from the template and
need to do nothing.
How to tell whether a tenant still needs it
Any one of these is sufficient:
- Open the Marketplace agreements view in FlowState. A 500 there, while products and offers load normally, is this migration.
- In the seller account: IAM → Roles →
FlowStateMarketplaceConnector→ Permissions. Look for a statement whoseActioncontainsaws-marketplace:SearchAgreements. Absent means the stack predates the change.
What to re-run
Re-run the connector stack the same way it was created. No resource is replaced — only the permission policy is updated. The role ARN, ExternalId and seller account are unchanged, so no re-verification and no reconnect is needed, and nothing else is interrupted.
-
CloudFormation: update the existing stack with the current
marketplace-connector.yaml. -
OpenTofu — if you used the published module (
sourcepointing attofu/marketplace-connector.zip), from that directory:tofu init -upgrade tofu apply-upgradeis the part that matters — it re-downloads the module zip. A plaintofu initkeeps the copy already in.terraform/modules/, so the apply would re-apply the old policy and report no changes. Your original-varvalues are already in state; you do not need to pass them again. Expect exactly one in-place update adding theMarketplaceAgreementReadstatement, nothing added or destroyed. -
OpenTofu — if you copied the inline files from Tab 1 (no module
source),-upgradehas nothing to re-download. Replace the policy document in yourmain.tfwith the current one from Tab 1 above, thentofu apply. -
Manual (IAM console): add the
MarketplaceAgreementReadstatement from the policy above to the role’s inline policy. Leave the existing statements in place.