When you build an integration or a nightly sync job against Salesforce B2C Commerce Cloud, a common requirement shows up almost immediately: pull only the records that changed since your last run, or only the ones valid within a date range. Catalogs, customer lists, orders — all of them need this at some point.

The tricky part isn’t the concept. It’s figuring out which API’s docs you should even be reading, and whether the query syntax for one carries over to the other. Both questions get answered below, then every filter type gets explained field by field.

Note

Updated July 2026: OCAPI is now officially deprecated. This refresh confirms which of the query syntax below still applies to the SCAPI, separates the one SCAPI surface that genuinely shares it from the one that doesn’t, and replaces the outdated “custom endpoints are BETA” section with the current, GA answer.

Querying

Not all endpoints are alike, but for date filtering, one pattern keeps showing up: a JSON Query document describes what to match, and an optional Filter narrows it down. Salesforce built the OCAPI Data API around this pattern years ago, and the newer SCAPI Admin Data API endpoints for Catalogs, Products, and Customers reuse the exact same document types and structure. The one thing that changes is spelling.

Note

This grammar is not universal across SCAPI, and the casing changes too. It applies to the SCAPI Admin Data API (system-to-system integrations authenticated with an Account Manager token): Salesforce ported the OCAPI Data API’s query documents over concept-for-concept, confirmed against the Query and Filter type references for Catalogs and Products. But the JSON isn’t byte-identical: every multi-word type name gets renamed to camelCase. filtered_query becomes filteredQuery, match_all_query becomes matchAllQuery, range_filter becomes rangeFilter, range2_filter becomes range2Filter, bool_filter becomes boolFilter, term_filter becomes termFilter, and term_query becomes termQuery. range2_filter’s own inner fields rename too, from from_field/to_field/filter_mode/from_value/to_value to fromField/toField/filterMode/fromValue/toValue. Every other field you’ll see below, field, from, to, operator, values, filters, fields, is a single word, so it’s spelled identically on both APIs. It does not apply at all to the shopper-facing Shopper Search API: that API takes no JSON query body at all. It filters through refine query-string parameters (refine=price=(0..100)) against attributes your catalog defines as searchable, and it has no general-purpose way to filter by an arbitrary date field like creation_date. If you’re building storefront search, that’s the API you want, and everything below this note won’t help you. If you’re syncing or auditing records from the back end, keep reading.

Here’s where those Query and Filter documents show up as real endpoints, paired with their SCAPI Admin Data API equivalent where one exists:

Warning

OCAPI is officially deprecated as of April 2026. Every OCAPI reference page now carries a “(deprecated)” label, and Salesforce ships new features to SCAPI only. The query knowledge in this article isn’t obsolete: the same documents still work if you’re maintaining an existing OCAPI integration, but a new integration should target the SCAPI Admin Data API endpoints above instead. See the OCAPI versus SCAPI rematch for the full migration picture and the maintenance-window timeline.

Attributes

Not every field on an object is filterable, and that’s set per endpoint, not globally. Before you write a query, check the documentation page for the specific endpoint (OCAPI or SCAPI) to see which attributes it actually supports as filter fields.

Date Format

Every date value in these filters must follow ISO 8601, written in UTC as YYYY-MM-DDTHH:MM:SSZ. Milliseconds are optional, not required: add them as YYYY-MM-DDTHH:MM:SS.mmmZ when you have that precision, but a plain-seconds timestamp parses just as well. The worked example below leaves them off for exactly that reason. Field names still have to match your data model, so confirm that creation_date, valid_from, valid_to, and similar names actually exist on the object you’re querying.

2012-03-19T07:22:59Z // example, no milliseconds needed

Range Filter

Calendars and ruler illustration introducing date-range filtering.

Use a range_filter when you have one date field and want everything that falls between a start and an end value. This is the filter behind the classic “give me everything created last week” sync query, and it’s documented as part of the shared Filter document type.

{
  "query": {
    "filtered_query": {
      "filter": {
        "range_filter": {
          "field": "creation_date",
          "from": "2020-03-08T00:00:00.000Z",
          "to": "2020-03-10T00:00:00.000Z"
        }
      },
      "query": {
        "match_all_query": {}
      }
    }
  }
}

Read it from the outside in:

  • filtered_query is the wrapper that says “run a query, then narrow the hits with a filter.” Most date-filtering queries use it for that reason. term_query, covered later, is the one exception: it doesn’t need a separate filter step.
  • filter.range_filter is where the actual date logic lives. field names the date attribute to check (creation_date here), and from/to set the interval’s boundaries.
  • query.match_all_query fills the “query” half of the wrapper. It matches every record unconditionally, so the range_filter ends up doing all the real work. You’ll see this same empty query in nearly every date-filtering example, because most of the time you don’t need a text or attribute query on top of the date range.

Leave out from or to and the range becomes open-ended on that side, though you can’t drop both at once: at least one boundary is required. A range with only from means “everything created on or after this date,” which is exactly what an incremental sync needs. Both boundaries are inclusive by default (fromInclusive/toInclusive, both default true), so a to of midnight includes anything created at exactly that instant; set either one to false if you need an exclusive bound instead.

Range2 Filter

A single range_filter breaks down the moment your data has two date fields instead of one, like a promotion’s valid_from/valid_to pair, and you need to know whether that validity window overlaps a period you care about. That’s what range2_filter is for.

It compares two ranges: R1, defined by a pair of fields on the record (from_field and to_field), against R2, defined by two literal values you supply (from_value and to_value). filter_mode sets the relationship the search has to satisfy:

  • overlap (the default if you omit filter_mode): R1 overlaps fully or partially with R2
  • containing: R1 contains R2
  • contained: R1 is contained in R2
"query" : {
     "filtered_query": {
        "filter": {
             "range2_filter": {
                 "from_field": "valid_from",
                 "to_field": "valid_to",
                 "filter_mode":"overlap",
                 "from_value": "2007-01-01T00:00:00.000Z",
                 "to_value": "2017-01-01T00:00:00.000Z"
             }
        },
        "query": { "match_all_query": {} }
    }
}

With filter_mode set to overlap, this query returns every record whose valid_fromvalid_to window touches the 2007–2017 range at all, even if it started years earlier or ends years later. Switch to contained and you’d only get records whose entire validity window sits inside that decade.

This is also the one filter in this article where the SCAPI Admin Data API version actually looks different on the page: the same request becomes range2Filter with fromField, toField, filterMode, fromValue, and toValue, same values, camelCase keys.

Bool Filter

A woman combining different blocks in a particular order.

Real-world date queries rarely stand alone. “Open orders created this year” needs a status check and a date range at the same time, and that’s a job neither range_filter nor term_filter can do by itself. bool_filter combines several filters into one logical expression.

{
  "query": {
    "filtered_query": {
      "query": {
        "match_all_query": {}
      },
      "filter": {
        "bool_filter": {
          "operator": "and",
          "filters": [
            {
              "term_filter": {
                "field": "status",
                "operator": "is",
                "values": ["open"]
              }
            },
            {
              "range_filter": {
                "field": "creation_date",
                "from": "2023-01-01T00:00:00.000Z"
              }
            }
          ]
        }
      }
    }
  }
}

bool_filter.operator sets how its filters array combines: and means a hit has to satisfy every entry, or means any single one is enough, and a third option, not (not shown here), negates the group; list more than one filter under not and it treats them as ANDed together first. Inside the array, each entry is a complete filter object in its own right, so you can mix filter types freely: this example pairs a term_filter (exact match on status) with the range_filter from the first section (an open-ended creation_date range with no to). The result: open orders created since the start of 2023, and nothing else.

That term_filter isn’t limited to exact matches either. It takes the same operator enum as term_query below, so less/greater/is_null/neq and friends work here too, not just is.

Term Query

range_filter and bool_filter narrow a search after it runs, through the filter half of filtered_query. Sometimes you don’t need a filter at all: you know the exact date a record was created and want to match it precisely. That’s term_query, and unlike the filters above, it belongs on the query side of the document.

{
  "query": {
    "term_query": {
      "fields": ["creation_date"],
      "operator": "is",
      "values": ["2023-04-01T00:00:00.000Z"]
    }
  }
}

fields takes an array because a term query can check more than one field at once (multiple fields are OR’d together, so a hit on any of them counts). operator controls how many values you’re allowed to supply: is accepts exactly one, while one_of accepts several and matches a record if any of them hits. Here, is with a single value in values means “creation_date must equal this exact instant”: useful for re-fetching one known record, not for the range-based syncs the earlier filters handle.

is and one_of aren’t the whole enum. operator also accepts is_null, is_not_null, less, greater, not_in, and neq, so a one-sided date comparison can go through term_query instead of range_filter if you’d rather. Not every endpoint supports less and greater here, so check the docs for the one you’re calling before you rely on them.

Custom Endpoint

None of the query types above cover every case. If you need an endpoint shaped entirely around your own requirements instead of an existing search resource, Custom APIs are the supported way to build one. They’ve been generally available since release 24.2, with real routing, a contract that validates every request before your code runs, and support for all HTTP methods, including transactions on POST, PUT, PATCH, and DELETE. That’s a solid step up from the GET-only, no-transaction workaround that used to be the only option here.

Performance and caching are still on you. The platform handles routing and validation; whether the endpoint ends up fast and cacheable depends entirely on what you write inside it.

Conclusion

range_filter, range2_filter, bool_filter, and term_query cover most date-filtering problems, on either the OCAPI Data API or the SCAPI Admin Data API, camelCased there as rangeFilter, range2Filter, boolFilter, and termQuery. Get the date format and field names right, and pick the filter that matches your question: one range, an overlap between two ranges, several conditions at once, or an exact match.

But that grammar isn’t the whole SCAPI story, and confusing it with the wrong surface costs real debugging time. Shopper-facing search runs on refine query-string parameters, not this JSON body, and OCAPI itself is on a maintenance clock now. Check which side of that line your integration sits on before you write the first query.