The 400 That Contradicts the Samples #
You're subscribing a dataset to an Amazon Marketing Stream destination. You send the body every reference implementation shows, and Amazon rejects it:
HTTP 400 Value null at 'destination.firehoseDestination.subscriberRoleArn'
The confusing part is that your body doesn't contain subscriberRoleArn at all, so "null" reads like Amazon is complaining about a field you never claimed to send. It is. The subscribe body now requires three ARNs, and older client code sends two.
If you built from an existing Marketing Stream integration, or from a sample written against the earlier API shape, this is where you land. The fix is one line — but the field naming is genuinely confusable, so it's worth being precise about what each ARN is.
The Three ARNs, and Which Is Which #
Two of the three differ by a single word, which is most of why this trips people:
| Field | What it points at |
|---|---|
deliveryStreamArn | Your Kinesis Data Firehose delivery stream — where the records land |
subscriptionRoleArn | The role Amazon assumes to write into your Firehose |
subscriberRoleArn | The role representing you as the subscriber — the newly required one |
body = {
"dataSetId": data_set_id,
"notes": "…",
"destinationArn": firehose_arn,
"destination": {
"firehoseDestination": {
"deliveryStreamArn": delivery_stream_arn,
"subscriptionRoleArn": subscription_role_arn,
"subscriberRoleArn": subscriber_role_arn, # required — omitting it 400s
}
},
}
Read the error message literally when this happens: the path it gives you (destination.firehoseDestination.subscriberRoleArn) is exactly where the missing key belongs. Amazon's own Marketing Stream documentation is the authority on the current shape — check it rather than an older integration, because that's the whole cause of this error.
While you're in the IAM console: the trust policy on the role Amazon assumes points at a fixed Amazon-owned AWS account ID. It's a constant published in the docs, not something you generate — don't substitute your own account number into it.
Next: Your Firehose Objects Are Probably Gzipped #
You fix the ARN, the subscription goes ACTIVE, records start landing in S3 — and your parser throws on the first object.
If compression is enabled on your Firehose delivery stream, objects arrive GZIP-compressed, with .gz keys. Reference parsers written against an uncompressed stream read plain JSON and fall over. Since whether compression is on depends on how your delivery stream was configured, this is inconsistent between two otherwise identical integrations.
Sniff rather than trust the file extension — S3 keys aren't always what you expect:
import gzip
def read_object(body: bytes) -> bytes:
# gzip magic number
if body[:2] == b"\x1f\x8b":
return gzip.decompress(body)
return body
Two lines, and it works whether or not compression is enabled — which also means it keeps working if someone changes that setting later.
Then: Your First Day Looks Empty #
The last surprise isn't a bug at all, but it generates a lot of "is this working?" energy on day one.
Marketing Stream is push-based and has no backfill. Data accrues from the moment your subscription becomes ACTIVE — there is no historical window, no request-past-data call, nothing to replay. Enrol on a Tuesday and Monday does not exist for you.
So if you need history, pull it from the Ads reporting API and stitch it to the stream at the enrolment boundary. And enrol earlier than you think you need to — every day you delay is a day of hourly granularity you can never get back. The reporting API can give you daily numbers for that period, but not the hourly detail that made you want Marketing Stream in the first place.
Setting this up from scratch? The full Firehose wiring is in the Marketing Stream setup guide, the cost model is in what AMS actually costs in AWS, and AMS vs AMC covers which one you actually want.
A Debugging Order That Saves Time #
When a Marketing Stream subscription isn't producing data, check in this order — it's roughly the order of likelihood, and each step is cheap:
- Is the subscription ACTIVE? Not created — active. A subscription can be created and then fail to activate because of the trust policy.
- Does the trust policy on the subscription role name Amazon's account correctly? This is the most common activation failure.
- Is Firehose actually delivering? Check the delivery stream's own CloudWatch metrics before suspecting Amazon. Delivery failures to S3 are visible there and have nothing to do with the Ads API.
- Is your parser silently skipping objects? The gzip case fails loudly, but a broad
exceptaround object parsing turns it into "no data" — which looks like a subscription problem and isn't. - Has enough time passed? Hourly datasets deliver hourly. An hour of nothing after enrolment is normal.
FAQ #
What causes “Value null at destination.firehoseDestination.subscriberRoleArn”?
Your subscribe request is missing the subscriberRoleArn field. The Marketing Stream subscribe body requires three ARNs — deliveryStreamArn, subscriptionRoleArn and subscriberRoleArn — and older client code and samples send only the first two.
What is the difference between subscriptionRoleArn and subscriberRoleArn?
subscriptionRoleArn is the IAM role Amazon assumes in order to write records into your Firehose delivery stream. subscriberRoleArn identifies you as the subscriber. They are separate fields and both are required.
Why does my Marketing Stream parser fail on the S3 objects?
If compression is enabled on your Firehose delivery stream, the objects are GZIP-compressed. Detect the gzip magic bytes 0x1f8b at the start of the object and decompress rather than assuming plain JSON.
Can I backfill historical Amazon Marketing Stream data?
No. Marketing Stream is push-based and delivers data only from the point your subscription becomes active. For any period before that you need the Amazon Ads reporting API, which gives daily rather than hourly granularity.
How long after subscribing should data appear?
For hourly datasets, expect the first records within about an hour of the subscription becoming ACTIVE. If nothing arrives after that, verify the subscription status and the delivery stream's CloudWatch metrics before assuming the subscribe call was wrong.
Related guides
Amazon AMS / Automation
How to Build Hourly Automated Bidding with Amazon Marketing Stream (AMS → BigQuery → Ads API)
A complete guide to pulling hourly conversion data from Amazon Marketing Stream (AMS), storing it in BigQuery, and automatically adjusting bids via Amazon Ads API based on real-time performance.
Read guide →Amazon Ads API
Amazon Ads API Rate Limits Explained (And How to Stop Hitting Them)
A 429 storm from the Amazon Ads API almost always means your retry logic is wrong, not that your limits are too low. Here's how the per-endpoint token buckets actually work, what the response headers tell you, and the request patterns that keep you comfortably under the limit.
Read guide →Amazon SP-API
The SP-API Reports Pattern Everyone Gets Wrong (Async, Step by Step)
The SP-API Reports API is asynchronous — request, poll, download, decompress — and most broken integrations either treat it like a synchronous call or poll it straight into a throttle. Here's the correct four-step flow, the status states that trip people up, and the gzip gotcha at the end.
Read guide →Don't want to build this yourself?
I set this up for sellers and agencies every week. Book a free 30-minute audit of your Amazon data & PPC setup — you'll leave with a plan either way, whether we work together or not.