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. Why the Reddit Ads API was harder than it looks 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 downloading 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 and conversion 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 paused 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 paused. 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 /pricing Read next Why your ad platforms disagree about last week /blog/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.