cd /news/developer-tools/the-pain-of-integration-with-the-red… · home topics developer-tools article
[ARTICLE · art-91638] src=growomat.com ↗ pub= topic=developer-tools verified=true sentiment=· neutral

The pain of integration with the Reddit API

Growomat's integration with the Reddit Ads API took two weeks instead of the estimated one day, due to undocumented requirements, contradictory documentation, and strict field semantics. The team found that the Reddit developer portal blocked automated documentation retrieval, forcing them to use the OpenAPI spec locally, and that error messages often pointed to unset fields, with null and absent values meaning different things. The experience highlights the need to rely on vendor examples over reference docs and to handle explicit nulls for inheritance.

read10 min views1 publishedAug 11, 2026
The pain of integration with the Reddit API
Image: source

We budgeted a day to integrate Reddit Ads and it took two weeks. Here are the refusals we hit, why our tests never caught them, and how it ends.

We assumed integrating Reddit ads would be a walk in the park. Boy, were we wrong.

After successfully setting up Google Ads and Microsoft Advertising with their stable interfaces, Reddit was the obvious next step for Growomat - especially since both our dev team and our users love the platform.

Things looked promising at first. We spent a week building the platform adapter and mock services, estimating that hooking up the live APIs would only take about a day.

Two weeks later, and a lot more hair and sleep loss, we realised it was more complicated than it looked.

If your team is integrating with the Reddit API, here are some lessons you can learn from.

The Reddit API is a new interface #

Claude and Codex know well-established APIs like Google Ads inside out. Reddit's they don't - there simply isn't much Reddit API material in their training data.

No problem - they should just pull the latest API documentation from the Reddit developer portal so they know what to build?

Oops - next problem, the Reddit developer portal blocked our automated tooling from retrieving the documentation.

We resolved this by down the full Reddit OpenAPI spec file and using that file locally for integration. This is a good learning to speed things up instead of a ton of online calls.

Once we got these basic interface issues sorted, we got to the next set of issues - weird error messages.

Weird error messages #

Because we care about our users' ad budgets and controlling their spending, we set budget optimization on (is_campaign_budget_optimization = true

).

However, this gave an error when creating the campaign:

Cap is required for Cost Cap campaigns

Campaigns with budget optimization turned on have to send bid_strategy

and bid_type

. Ours sent neither, because the user hadn't configured any bidding at all. What came back wasn't "bid_strategy is required".

So we thought - let's just set the automatic bidding strategy, MAXIMIZE_VOLUME

. However, it isn't; it is the Cost Cap product, and it wants a cap. The automatic one is BIDLESS

, which is what Reddit uses in every template in its own campaign setup guide.

The OpenAPI description, meanwhile, says the cap is "Optional when bid_strategy is MAXIMIZE_VOLUME", contradicting both the guide and the live API. Believing it cost us a lot of debugging.

So when a platform complains about a field you never set, go looking for a default you never chose. And when the reference disagrees with the vendor's own worked examples, back the examples; they usually describe the system that's actually running.

Absent, null and "inherit" are three different things #

Campaign accepted. We moved down a level, and Reddit refused the ad group:

missing required attributes: bid_strategy, bid_type

Under budget optimization the money lives on the campaign, so why does the ad group need bidding fields at all?

Because bid_strategy

and bid_value

have to go up as explicit nulls, and that null is doing real work. It's how an ad group says "inherit from the campaign". Leaving the keys out isn't the same as sending them empty. Our compact()

helper strips nulls, so we had to attach these fields after it ran.

Then the same distinction bit us from the other direction:

Ad group field optimization_goal must match the campaign value.

We were computing that field in two places under two different rules, so a clicks campaign went up with no goal at all while its ad group confidently asserted one.

The fix wasn't a better default. It was to send nothing at all in the normal case. Campaigns already on Reddit were created before we sent that field and hold whatever Reddit derived for them, so anything we invent might disagree. Sending nothing can't disagree with anything, and the ad group inherits.

Lesson: find out whether your API distinguishes "key absent" from "key present, value null". Your JSON serializer already has an opinion, and you probably haven't read it.

One payload for create and update #

We compiled a single payload and used it for both verbs. Two bugs came out of that, and both stayed invisible until something got pushed a second time.

The first: our update carried create-only fields. Reddit accepts objective

and is_campaign_budget_optimization

when you create a campaign, and rejects the entire PATCH if either one comes back. Every update of a linked campaign 400'd.

The second is nastier. Our model has no start date, so start_time

was synthesized as "now" during translation, which is correct exactly once. Every later PATCH synthesized a fresher "now" and marched the campaign's start past ad groups created minutes or days earlier:

Campaign start time cannot be after the ad group start time.

So a second push of any Reddit campaign without an explicit start date could never succeed. And guess what counts as a second push? Enabling a campaign - quite important.

Lesson: anything that's only true at creation time belongs at the create boundary, not in the code that assembles the payload.

Deleting is a ladder, and it has a timer on it #

You can't delete a Reddit campaign:

A campaign must be archived before it can be deleted.

Fine. Archive it, then delete it:

A campaign cannot be deleted until three hours after it was archived.

Three hours. That second refusal is unavoidable on a fresh teardown, so no removal could ever finish.

Then it got worse. We rethrew the error, so the local cleanup never ran, the tombstone sat there, and the removal fired again on the next sync. Those failures counted toward our circuit breaker, and five of them switched off a user's entire sync schedule - every platform, every campaign - over one deleted Reddit campaign.

The fix was realising that archiving is already enough. ARCHIVED maps to our canonical removed state, so removal completes there, and the delete itself is a best-effort reap we ignore if it fails.

There's a matching trap on reads. Reddit has no hard delete, so a removed campaign keeps turning up in list responses as configured_status: DELETED

forever. Once a removal finished, that tombstone was just an unmatched remote object, and we cheerfully imported it back as a brand new campaign.

Lesson: find out what a platform's terminal states really are before you model deletion as a verb, and go and look at what a deleted object does in a list response.

Asynchronous creatives change more than the client #

Reddit creatives aren't fields on the ad. They're a job you submit to /profiles/{profile_id}/structured_posts/jobs

and then poll.

We sent our own flat internal shape for weeks, and every ad this product ever tried to publish on Reddit failed at that job, on every sync, with the same refusal:

400 data/creative: "'creative' is a required property"

The real schema nests a creative

object, and it speaks a different vocabulary for each type. A TEXT creative has no destination at all, because a text ad's click comes from the ad's own click_url

. An IMAGE creative carries a typed destination object holding the call to action, and CAROUSEL holds an array of those. profile_id

is read-only in the body, since the path already carries it.

A 200 doesn't mean anything is running #

This is the one I'd most want somebody else to avoid.

funding_instrument_id

was written nowhere in our codebase. Not a bug we'd introduced - a field we had never implemented at all. Reddit accepted every campaign we created, returned success, and then quietly parked each one at:

delivery_status: ["CAMPAIGN_NOT_FUNDED"]

Forever. Every campaign we had ever created on Reddit was unservable, and nothing in any response we looked at said so.

The prerequisite check meant to catch this asked whether the account had a servable funding instrument. It did, the entire time it was broken. Account billing and campaign binding are different facts, and only the second one delivers ads.

Lesson: a 200 means the platform accepted your request. It doesn't mean anything is running. If a platform publishes a delivery or status field, read it, and treat "we reported success" and "we verified it works" as different claims.

The real problem: our mock agreed with us #

Read back over those sections and the same sentence is hiding in all of them. Every one of those shipped through a test suite that was green.

Here's what our mock was doing:

  • For reporting, it echoed back conversions

andconversion_value

, two field names our own request had invented. Neither exists on Reddit. The mock was validating our assumption instead of Reddit's contract, so it could never fail. - It overrode the create methods outright and never reached the real transport, so every test passed against a body Reddit rejects.

  • It overrode creative handling wholesale, so the actual wire body didn't exist in any test.
  • It accepted any optimization goal, so every test exported a tree that a real account refuses at the first ad group.
  • It hard-deleted rows that Reddit only tombstones.

We also had a recorded payload snapshot asserting a value a real account refuses. That baseline wasn't stale; it was wrong on the day it was recorded, and it had been passing happily ever since.

Which is the actual point of all this. A mock you wrote from the documentation is a machine-readable copy of your assumptions, and it will go on agreeing with your code for exactly as long as your assumptions are the broken thing.

So we stopped relying on it. We now run a live harness against a real ad account that drives the whole stack - public API, planner, queued orchestrator, real adapter, real platform - and verifies every write by reading the platform back instead of trusting our own success report. Everything it creates is d and never enabled, every object carries a greppable run stamp, and teardown checks itself by re-reading the account.

That distinction earned itself on the first proper run. One Reddit sync had been reporting success, errors: 0, counts.ads: 1

while changing nothing at all. The harness found three defects a fully green suite had never seen, and two of them were in the sync orchestrator rather than in the adapter, so an adapter-level test would have missed them too.

Where we landed #

Worth saying how this ends, because the fortnight bought something.

Creating and shipping a Reddit campaign is now one conversation. Growomat exposes its whole API as tools, so this is the entire flow:

> Build a Reddit campaign for /without-an-agency and push it live.

create_campaign    → campaign created
create_ad_group    → ad group + community targeting
create_ad          → 3 free-form text ads
validate_export    → valid · 0 blocking findings
trigger_sync       → platforms: ["reddit-ads"]
get_sync_status    → completed · errors: 0

Everything lands on Reddit d. You look at it, then you enable it. We enabled that one, and in its first five days it served 8,828 impressions and took 57 clicks.

None of the refusals above happen any more. They're all encoded now - in the adapter, in a conformance suite that checks every method, path and envelope against Reddit's published spec, and in a mock that refuses in Reddit's own words when we get it wrong.

The client took a week. The fortnight afterwards, where a real account told us everything we'd got wrong, was the actual integration. That part never shows up in anybody's getting started guide.

Run all four platforms from one place. #

Build, optimize and report on Google, Microsoft, Reddit and Meta campaigns without an agency retainer.

See what it costs

Read next

Why your ad platforms disagree about last week

Google, Microsoft, Reddit and Meta each close the day and count a conversion on their own terms. Here is exactly where the numbers split, and what to do about it.

── more in #developer-tools 4 stories · sorted by recency
── more on @growomat 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/the-pain-of-integrat…] indexed:0 read:10min 2026-08-11 ·