# Filtering Learn how to filter resources using flexible filter criteria with AND/OR logic. ## Overview All MVMNT filter endpoints support powerful filtering capabilities that allow you to query resources using flexible criteria with boolean logic. Filtering is consistent across all resource types (orders, vendors, carriers, customers, etc.). ## How Filtering Works ### Filter Endpoints Resources are queried using POST requests to `/filter` endpoints: ```bash POST /v1/vendors/filter POST /v1/carriers/filter POST /v1/customers/filter POST /v1/orders/filter ``` ### Request Structure ```json { "filter": { "status": { "equalTo": "ACTIVE" }, "createdAt": { "greaterThan": "2025-01-01T00:00:00Z" } }, "pageSize": 50, "cursor": null } ``` ### Default Behavior **Important:** By default, only non-deleted records are returned: ```json { "filter": { "deletedAt": { "isNull": true } } } ``` To include deleted records, explicitly override the `deletedAt` filter (see [Soft Deletes](/getting-started/soft-deletes)). ## Filter Types Different field types support different filter operations: ### StringFilter For text fields like `name`, `email`, `phone`, `notes` | Operation | Description | Example | | --- | --- | --- | | `equalTo` | Exact match | `{ "equalTo": "ABC Corp" }` | | `notEqualTo` | Not equal to | `{ "notEqualTo": "XYZ Inc" }` | | `in` | Matches any value in array | `{ "in": ["ABC", "XYZ"] }` | | `notIn` | Does not match any value in array | `{ "notIn": ["Test", "Demo"] }` | | `includes` | Contains substring (case-insensitive) | `{ "includes": "warehouse" }` | | `notIncludes` | Does not contain substring | `{ "notIncludes": "test" }` | | `startsWith` | Starts with prefix (case-insensitive) | `{ "startsWith": "ABC" }` | | `notStartsWith` | Does not start with prefix | `{ "notStartsWith": "test" }` | | `endsWith` | Ends with suffix (case-insensitive) | `{ "endsWith": ".com" }` | | `notEndsWith` | Does not end with suffix | `{ "notEndsWith": ".test" }` | | `isNull` | Field is null or not null | `{ "isNull": true }` or `{ "isNull": false }` | **Example:** ```json { "filter": { "name": { "includes": "warehouse" }, "email": { "endsWith": "@example.com" } } } ``` ### IntFilter For integer fields like counts, quantities | Operation | Description | Example | | --- | --- | --- | | `equalTo` | Exact match | `{ "equalTo": 100 }` | | `notEqualTo` | Not equal to | `{ "notEqualTo": 0 }` | | `lessThan` | Less than | `{ "lessThan": 50 }` | | `lessThanOrEqualTo` | Less than or equal | `{ "lessThanOrEqualTo": 100 }` | | `greaterThan` | Greater than | `{ "greaterThan": 0 }` | | `greaterThanOrEqualTo` | Greater or equal | `{ "greaterThanOrEqualTo": 10 }` | | `in` | Matches any value | `{ "in": [1, 2, 3] }` | | `notIn` | Does not match any | `{ "notIn": [0, -1] }` | | `isNull` | Field is null | `{ "isNull": false }` | **Example:** ```json { "filter": { "quantity": { "greaterThan": 0 }, "weight": { "lessThanOrEqualTo": 10000 } } } ``` ### FloatFilter For decimal fields like prices, weights, dimensions Same operations as `IntFilter`, but accepts float values: ```json { "filter": { "price": { "greaterThanOrEqualTo": 99.99 }, "weight": { "lessThan": 500.5 } } } ``` ### BooleanFilter For true/false fields like `isMvmnt`, `isPrimary` | Operation | Description | Example | | --- | --- | --- | | `equalTo` | Exact match | `{ "equalTo": true }` | | `notEqualTo` | Not equal to | `{ "notEqualTo": false }` | | `isNull` | Field is null | `{ "isNull": false }` | **Example:** ```json { "filter": { "isPrimary": { "equalTo": true }, "isActive": { "notEqualTo": false } } } ``` ### DatetimeFilter For date/time fields like `createdAt`, `updatedAt`, `deletedAt` | Operation | Description | Example | | --- | --- | --- | | `equalTo` | Exact match | `{ "equalTo": "2025-01-15T10:00:00Z" }` | | `notEqualTo` | Not equal to | `{ "notEqualTo": "2025-01-01T00:00:00Z" }` | | `lessThan` | Before datetime | `{ "lessThan": "2025-02-01T00:00:00Z" }` | | `lessThanOrEqualTo` | On or before | `{ "lessThanOrEqualTo": "2025-01-31T23:59:59Z" }` | | `greaterThan` | After datetime | `{ "greaterThan": "2025-01-01T00:00:00Z" }` | | `greaterThanOrEqualTo` | On or after | `{ "greaterThanOrEqualTo": "2025-01-01T00:00:00Z" }` | | `isNull` | Field is null | `{ "isNull": true }` | **Example:** ```json { "filter": { "createdAt": { "greaterThanOrEqualTo": "2025-01-01T00:00:00Z", "lessThan": "2025-02-01T00:00:00Z" } } } ``` **Note:** Use ISO 8601 format (RFC 3339) for all datetime values. ### UUIDFilter For UUID fields like foreign keys (`paymentTermId`, `deletedById`) | Operation | Description | Example | | --- | --- | --- | | `equalTo` | Exact match | `{ "equalTo": "550e8400-e29b-41d4-a716-446655440000" }` | | `notEqualTo` | Not equal to | `{ "notEqualTo": "..." }` | | `in` | Matches any UUID | `{ "in": ["uuid1", "uuid2"] }` | | `notIn` | Does not match any | `{ "notIn": ["uuid1"] }` | | `isNull` | Field is null | `{ "isNull": false }` | **Example:** ```json { "filter": { "paymentTermId": { "in": [ "550e8400-e29b-41d4-a716-446655440000", "660e8400-e29b-41d4-a716-446655440001" ] } } } ``` ### IDFilter For the primary `id` field (limited operations) | Operation | Description | Example | | --- | --- | --- | | `equalTo` | Exact match | `{ "equalTo": "550e8400-..." }` | | `in` | Matches any UUID | `{ "in": ["uuid1", "uuid2"] }` | **Example:** ```json { "filter": { "id": { "in": [ "550e8400-e29b-41d4-a716-446655440000", "660e8400-e29b-41d4-a716-446655440001" ] } } } ``` **Note:** For single ID lookups, use `GET /{resource}/{id}` instead of filtering. ### ClientKeyFilter For the `key` field (limited operations) | Operation | Description | Example | | --- | --- | --- | | `equalTo` | Exact match | `{ "equalTo": "ERP-001" }` | | `in` | Matches any key | `{ "in": ["ERP-001", "ERP-002"] }` | | `isNull` | Field is null | `{ "isNull": false }` | **Example:** ```json { "filter": { "key": { "equalTo": "ERP-VENDOR-123" } } } ``` **Note:** For single key lookups, use the simpler query parameter: `GET /vendors?key=ERP-VENDOR-123` ## Logical Operators Combine filters using boolean logic: ### AND Logic (Default) Multiple filters at the same level are implicitly AND-ed: ```json { "filter": { "status": { "equalTo": "ACTIVE" }, "currency": { "equalTo": "USD" } } } ``` This matches vendors that are **both** ACTIVE **and** use USD currency. ### Explicit AND Use the `and` operator for clarity or nested conditions: ```json { "filter": { "and": [ { "status": { "equalTo": "ACTIVE" } }, { "currency": { "equalTo": "USD" } } ] } } ``` ### OR Logic Use the `or` operator to match any condition: ```json { "filter": { "or": [ { "status": { "equalTo": "ACTIVE" } }, { "status": { "equalTo": "PENDING" } } ] } } ``` This matches vendors that are **either** ACTIVE **or** PENDING. ### NOT Logic Use the `not` operator to negate a condition: ```json { "filter": { "not": { "status": { "equalTo": "INACTIVE" } } } } ``` This matches vendors that are **not** INACTIVE (includes ACTIVE, PENDING, null, etc.). ### Complex Logic Combine operators for sophisticated queries: ```json { "filter": { "and": [ { "or": [ { "status": { "equalTo": "ACTIVE" } }, { "status": { "equalTo": "PENDING" } } ] }, { "or": [ { "currency": { "equalTo": "USD" } }, { "currency": { "equalTo": "CAD" } } ] } ] } } ``` This matches vendors that are **(ACTIVE or PENDING) and (USD or CAD)**. ## Common Patterns ### Pattern 1: Find Active Records Created Recently ```json { "filter": { "status": { "equalTo": "ACTIVE" }, "createdAt": { "greaterThanOrEqualTo": "2025-01-01T00:00:00Z" } } } ``` ### Pattern 2: Search by Name or Email ```json { "filter": { "or": [ { "name": { "includes": "warehouse" } }, { "email": { "includes": "warehouse" } } ] } } ``` ### Pattern 3: Exclude Test/Demo Records ```json { "filter": { "and": [ { "name": { "notIncludes": "test" } }, { "name": { "notIncludes": "demo" } }, { "email": { "notEndsWith": ".test" } } ] } } ``` ### Pattern 4: Find Records Without a Relationship ```json { "filter": { "paymentTermId": { "isNull": true } } } ``` ### Pattern 5: Find Records Modified in Date Range ```json { "filter": { "updatedAt": { "greaterThanOrEqualTo": "2025-01-01T00:00:00Z", "lessThan": "2025-02-01T00:00:00Z" } } } ``` ### Pattern 6: Find by Multiple Client Keys ```json { "filter": { "key": { "in": [ "ERP-VENDOR-001", "ERP-VENDOR-002", "ERP-VENDOR-003" ] } } } ``` ### Pattern 7: Exclude Specific IDs ```json { "filter": { "id": { "notIn": [ "550e8400-e29b-41d4-a716-446655440000", "660e8400-e29b-41d4-a716-446655440001" ] } } } ``` ## Enum Filters Enum fields (like `status`, `currency`, `roles`) have specialized filters: ```json { "filter": { "status": { "equalTo": "ACTIVE", "in": ["ACTIVE", "PENDING"], "notEqualTo": "INACTIVE", "notIn": ["INACTIVE", "DELETED"], "isNull": false } } } ``` See resource-specific documentation for available enum values. ## Array Field Filters For array fields (like contact `roles`), use specialized operators: ```json { "filter": { "roles": { "includes": "BILLING", // Array contains this value "notIncludes": "ARCHIVED", // Array does not contain this value "isNull": false // Field is not null } } } ``` ## Complete Examples ### Example 1: Find Active Vendors in Specific States ```bash curl -X POST https://api.mvmnt.io/v1/vendors/filter \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "filter": { "and": [ { "status": { "equalTo": "ACTIVE" } }, { "or": [ { "corporateAddress": { "state": { "equalTo": "CA" } } }, { "corporateAddress": { "state": { "equalTo": "TX" } } } ] } ] }, "pageSize": 100 }' ``` ### Example 2: Find Contacts Created Last 30 Days ```bash curl -X POST https://api.mvmnt.io/v1/vendor-contacts/filter \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{ "filter": { "createdAt": { "greaterThanOrEqualTo": "2025-01-15T00:00:00Z" } }, "pageSize": 50 }' ``` ### Example 3: Complex Business Query Find vendors that: - Are ACTIVE or PENDING - Use USD or CAD currency - Were created in 2025 - Are not test vendors - Have a payment term set ```json { "filter": { "and": [ { "or": [ { "status": { "equalTo": "ACTIVE" } }, { "status": { "equalTo": "PENDING" } } ] }, { "or": [ { "currency": { "equalTo": "USD" } }, { "currency": { "equalTo": "CAD" } } ] }, { "createdAt": { "greaterThanOrEqualTo": "2025-01-01T00:00:00Z", "lessThan": "2026-01-01T00:00:00Z" } }, { "name": { "notIncludes": "test" } }, { "paymentTermId": { "isNull": false } } ] } } ``` ### Example 4: JavaScript Helper ```javascript async function filterVendors(criteria) { const response = await fetch('https://api.mvmnt.io/v1/vendors/filter', { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ filter: criteria, pageSize: 250, }), }); return response.json(); } // Find active USD vendors const activeUsdVendors = await filterVendors({ and: [ { status: { equalTo: 'ACTIVE' } }, { currency: { equalTo: 'USD' } }, ], }); // Find vendors by name pattern const warehouseVendors = await filterVendors({ name: { includes: 'warehouse' }, }); // Find recently created vendors const recentVendors = await filterVendors({ createdAt: { greaterThanOrEqualTo: '2025-01-01T00:00:00Z', }, }); ``` ### Example 5: Python Helper ```python import requests from typing import Dict, List from datetime import datetime, timedelta def filter_vendors( access_token: str, filter_criteria: Dict, page_size: int = 250 ) -> List[Dict]: """Filter vendors using given criteria""" response = requests.post( 'https://api.mvmnt.io/v1/vendors/filter', headers={ 'Authorization': f'Bearer {access_token}', 'Content-Type': 'application/json', }, json={ 'filter': filter_criteria, 'pageSize': page_size, } ) response.raise_for_status() result = response.json() return result['data'] # Find active vendors active_vendors = filter_vendors( token, {'status': {'equalTo': 'ACTIVE'}} ) # Find vendors created in last 7 days week_ago = (datetime.utcnow() - timedelta(days=7)).isoformat() + 'Z' recent_vendors = filter_vendors( token, {'createdAt': {'greaterThanOrEqualTo': week_ago}} ) # Complex query complex_result = filter_vendors( token, { 'and': [ {'status': {'equalTo': 'ACTIVE'}}, {'currency': {'in': ['USD', 'CAD']}}, {'name': {'notIncludes': 'test'}} ] } ) ``` ## Best Practices ### ✅ Do - **Use specific filters**: More specific filters are more efficient - **Filter on indexed fields**: `id`, `key`, `status`, timestamps are indexed - **Combine with pagination**: Always use with proper pagination for large results - **Use `in` for multiple values**: More efficient than multiple `or` conditions - **Leverage default deletedAt filter**: Let the API filter deleted records automatically - **Use ISO 8601 for dates**: Always use RFC 3339 format for datetime fields ### ❌ Don't - **Don't use `includes` on large text fields**: Can be slow, prefer exact matches when possible - **Don't create overly complex filters**: Break into multiple simpler queries if needed - **Don't filter when you can use direct lookup**: Use `GET /{id}` or `?key=` for single records - **Don't forget about deleted records**: Override `deletedAt` filter if you need them - **Don't use filters for counting**: Use dedicated count/stats endpoints when available - **Don't mix AND/OR at the same level**: Use explicit `and`/`or` arrays for clarity ## Performance Tips ### Optimize for Speed 1. **Use indexed fields first**: `id`, `key`, `status`, timestamps 2. **Limit result set**: Use specific filters before pagination 3. **Use `equalTo` when possible**: Faster than substring matches 4. **Avoid `includes` on large datasets**: Use more specific filters 5. **Leverage caching**: Results are internally cached for pagination ### Query Efficiency ```json // ✅ Efficient - uses indexed fields first { "filter": { "and": [ { "status": { "equalTo": "ACTIVE" } }, { "name": { "includes": "warehouse" } } ] } } // ❌ Less efficient - substring search first { "filter": { "name": { "includes": "warehouse" } } } ``` ## Troubleshooting ### No Results Returned **Problem:** Filter returns empty `data` array **Possible Causes:** 1. Filter criteria too restrictive 2. All matching records are deleted 3. Typo in filter values 4. Wrong field name **Solutions:** 1. Simplify filter criteria progressively 2. Override `deletedAt` filter to include deleted records 3. Verify filter values match expected format 4. Check OpenAPI schema for correct field names ### Too Many Results **Problem:** Filter returns more results than expected **Possible Causes:** 1. Filter too broad 2. Case-insensitive matching on `includes` 3. Unexpected null values matching `isNull: true` **Solutions:** 1. Add more specific filter criteria 2. Use `equalTo` instead of `includes` for exact matches 3. Add null checks where appropriate ### Filter Not Working **Problem:** Filter appears ignored **Possible Causes:** 1. Wrong filter operator for field type 2. Invalid filter structure 3. Field doesn't support that filter type 4. Default `deletedAt` filter overriding your filter **Solutions:** 1. Check field type and use appropriate filter operators 2. Validate JSON structure matches examples 3. Consult OpenAPI schema for supported filter types 4. Explicitly set `deletedAt` filter if needed ### Date Filter Issues **Problem:** Date filters not matching expected results **Possible Causes:** 1. Wrong datetime format 2. Timezone confusion 3. Using `equalTo` instead of range operators **Solutions:** 1. Always use ISO 8601 / RFC 3339 format 2. Use UTC timezone (Z suffix) 3. Use `greaterThan`/`lessThan` for date ranges ## Next Steps - [Pagination](/getting-started/pagination) - Paginate through filtered results - [Soft Deletes](/getting-started/soft-deletes) - Understand how deleted records work - [Client Keys](/getting-started/client-keys) - Filter by your own identifiers - [API Reference](/apis/openapi) - See available filters for each resource type