How to Migrate Outreach API Integrations Off Relationship-Filter and Include-On-Create Deprecations
Outreach deprecated relationship filtering, include-on-create, and the implicit count metadata. Here's how to migrate each one without doubling your 429 rate.
If your Outreach integration was written before late 2023, three things it used to do silently no longer work the way you remember - and the failure mode is the worst kind: empty arrays, missing meta.count fields, and 200 OKs where you used to get filtered data. The deprecations have been on the books since May 2023, the enforcement deadlines passed in late 2023 for two of them and February 12, 2025 for the third, and the official deprecation page is now the canonical reference. I've spent the last week migrating a customer's sync job off all three, and the patterns below are what actually shipped.
The three deprecated behaviors are relationship-attribute filtering, relationship-attribute sorting, and including related objects in the response of a create or update. The fourth thing that bit us - because nobody read the changelog - was the count=true default flip, which means collection queries returning "count": null in their meta envelope are not broken, they're just on the new default.
Audit your integration for the four affected patterns
Before touching anything, grep your codebase for the call shapes that broke. The deprecated filter pattern looks like ?filter[prospect][company][name]=Acme - any filter key with more than one set of brackets is filtering through a relationship and will return empty results on endpoints where the relation chain is no longer supported. Sorting by a relationship looks like ?sort=-prospect.createdAt with a dot in the field. Includes-on-create are POST bodies followed by ?include=account,owner on the create call itself - the create succeeds, but the response body no longer carries the related objects.
The count flip is harder to grep because the change is server-side. Pull a single response from a list endpoint and look at meta.count: if it's null, you're on the new default and any UI element showing "X total prospects" is silently wrong. The Outreach support FAQ states the change applied to new applications from October 13, 2024 and to existing applications from February 12, 2025, with no further grace period.
"Outreach identified these behaviors as introducing performance and data inconsistency issues. The organization determined the problems outweighed the benefits. [...] Filters by relationship's attribute for some of the endpoints or relations" are no longer supported; developers must "perform two sequential requests - first querying for relationship IDs, then filtering using those IDs."
The "for some of the endpoints" hedge is doing a lot of work. The deprecation isn't uniform - some endpoint pairs still allow relationship filtering, others don't, and the Outreach docs don't publish a single matrix. I believe the safe assumption is to treat all relationship filters as gone and pay the extra request, rather than maintain a per-endpoint whitelist you'll forget to update when Outreach extends the deprecation later.
Fix rate-limit handling and the count default first
This is the cheap pre-work and it pays for itself within an hour. Outreach enforces a per-user limit of 10,000 requests per hour, and every response carries three headers: X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (or Retry-After on 429s). The two-step rewrites that come next will roughly double your request volume on filter-heavy syncs - if you don't have backoff in place, the rewrite itself trips 429s and looks like the deprecation broke things.
The other rate-limit gotcha is access-token retrieval, capped at one token per user per 60 seconds. Long-running sync jobs that re-authenticate on every page hit a second 429 - a token-retrieval 429, not a data 429 - and silently stall. Cache the token until it actually expires; don't refresh defensively. Kaia is also worth flagging: 3 API calls per second and 6,000 per day at the organization level, which doesn't bite until you try to backfill twelve months of transcripts.
The count flip is mechanical. Anywhere your integration reads response.meta.count for a "total matching rows" number, append &count=true to the query string. The new shape looks like /prospects?filter[stage]=cold&count=true. If a sync doesn't need a count (you're paginating through every row anyway), don't add it back - the count costs Outreach more, which is why they flipped the default.
Replace relationship filters with a two-step fetch
The old pattern - "find all tasks belonging to prospects at companies named Acme" - was one request: GET /tasks?filter[prospect][company][name]=Acme. The new pattern is two requests, and the cleanest implementation looks like this in pseudocode:
company_ids = GET /accounts?filter[name]=Acme -> collect ids
prospect_ids = GET /prospects?filter[account][id]=<company_ids> -> collect ids
tasks = GET /tasks?filter[prospect][id]=<prospect_ids>
The depth matters: filtering by an immediate parent's ID (one hop) is still allowed because id is a primitive on the parent, not a relationship-attribute traversal. Filtering by the parent's parent's name (two hops, ending in a string attribute) is what got deprecated. In practice almost every multi-hop filter we found was reachable in two requests instead of three, because the first hop's IDs are usually the only thing you actually need.
The other win from this rewrite is that you can now batch the second request by passing a comma-separated list of IDs, which the single-request form couldn't do efficiently. This pattern is also the same shape we used in how to stop Salesforce duplicate rules failing silently - whenever a CRM API silently swallows a query that used to work, the fix is almost always to add an explicit two-step lookup so the failure surfaces.

Split include-on-create into a POST then a GET
The third deprecation bites integrations the hardest because it changes a tightly-coupled write/read into two operations that can interleave with other traffic. The old shape was POST /prospects?include=account,owner - the create returned the new prospect plus its account and owner relations in one envelope, and your application could write the whole graph to local state in one transaction.
The new shape: POST /prospects returns just the prospect ID and its primitive attributes; a follow-up GET /prospects/{id}?include=account,owner pulls the related objects. Two requests, and your local-state code now has to handle the brief window between them. We work around this by writing the prospect to local state on the POST response, marking it as "relations-pending," and reconciling on the GET. For high-volume creates - 100+ prospects in a batch - the cleanest pattern is to POST all creates first, collect IDs, then issue one GET /prospects?filter[id]=<ids>&include=account,owner to fetch every relation in one call.
This is where the Leadex tie-in lands honestly. Leadex sits at the seam between discovery and CRM sync - the agent finds prospects, enriches them via your Apollo or Cognism key, and pushes results to HubSpot or Outreach - and the Outreach push had to be rewritten exactly along these lines when we shipped the integration last quarter. The plan-preview surface shows the create-then-get pattern as two distinct steps in the run log, which makes the new behavior visible to the user rather than hidden inside a single black-box API call.
Test against a sandbox before the next sync cycle
Outreach offers a developer sandbox that mirrors the production deprecations exactly. Two things to verify there: (1) every meta.count read in your code returns a real integer rather than null, which proves you remembered the &count=true on that endpoint; (2) every two-step filter rewrite returns the same row set as the deprecated single-request version returned in a pre-2023 snapshot. The second check is harder because the old behavior isn't reachable anymore - in practice you spot-check against your CRM-side truth.
One subtlety: response shape for two-step filters differs when the join is empty. The deprecated single request returned an empty data array; the two-step pattern returns an empty array on the first request and then doesn't issue the second request at all. Make sure your iteration code handles the "no IDs to fetch" branch explicitly rather than passing an empty list to the second filter, which Outreach interprets as "filter by nothing" and returns every row in the table. (!) The fix is a one-line guard - I've now seen this bug twice in the wild, both on systems that hadn't run their integration tests against an empty match in years.
For longer-term resilience, our running list of Outreach API gotchas lives next to our notes on how to audit ZoomInfo data freshness before renewal and how to audit HubSpot Breeze prospecting agent drafts; the throughline is that vendor-side defaults change quietly, and the integrations that survive are the ones whose owners read the deprecation page on the first of every quarter.
FAQ
What does the empty count field mean on an Outreach API response?
The meta.count field returns null instead of a row count when the request was made without an explicit &count=true parameter under the new default behavior, in effect for existing applications since February 12, 2025. The data itself is still correct - only the total-rows metadata is missing. Append &count=true to the query string to restore the integer count.
Are relationship filters supported on any Outreach API endpoint after the deprecation?
Some endpoint pairs still allow filtering by an immediate parent's primitive attributes (most often id), but filtering through two or more hops or by a relationship's string attribute is no longer supported across the API. The safest assumption is to treat every multi-hop filter as deprecated and rewrite it as a two-step fetch.
Does the include-on-create deprecation affect PATCH as well as POST?
Yes - the same deprecation applies to both create (POST) and update (PATCH) operations. The response no longer carries related objects regardless of which write verb you used. The migration is the same in both cases: do the write, capture the returned ID, then issue a follow-up GET with the ?include= parameter for the related objects.
What is the request-rate ceiling for Outreach API integrations in 2026?
The per-user limit is 10,000 requests per hour, enforced via the X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. Bulk and Kaia endpoints have separate, tighter limits - Kaia is capped at 3 calls per second and 6,000 calls per day at the organization level. Access-token retrieval is capped at once per user per 60 seconds.
How do I verify my Outreach integration is fully migrated before the next sync cycle?
Run a full sync against the Outreach developer sandbox and verify three things: every collection query that reads meta.count returns an integer (proves &count=true coverage), every former relationship filter returns the same row set as a CRM-side truth check, and your iteration code handles the empty-IDs branch on two-step filters without sending an unconstrained second request. Match those three and the production migration will be uneventful.