Skip to content
API PlatformDevelopers
POST/v1/saved-filtersbeta

Save a contact filter

Needs
Create and edit contacts, tags, contact groups and custom fields (crm:write)
Plan
Any plan with API access
Limits
60 a minute per key
Dry run
Yes — a full preview with ?dry_run=true
Undo
Delete it with Delete a saved filter.

Saves an advanced contact filter under a name. Your whole team sees it, it opens in the app's contacts page, and Search contacts can apply it by id.

It is checked first, like Check a contact filter. Relative dates are saved as the absolute dates they mean now.

Try it with ?dry_run=true first: every check the save runs, and the answer is the saved filter exactly as it would be stored (your name, your filter with its dates made absolute, id: null); nothing is saved and no Idempotency-Key is needed. Add count=true to also count the contacts it would match; if counting fails, the preview still answers (count: null and count_error).

Try it

Query (1)

With `dry_run=true`: also count the contacts it would match.

Body

Name shown in the app (1–255 characters).

84 / 16,384 bytes as JSON

Limits: at most 8 levels deep, 60 conditions, 500 items per list, 500 characters per value, regex ≤ 200 characters, 16 KB in all. Format, operators and recipes: Filtering contacts.

The filter to save (the **Search contacts** format).

Optional description (up to 1000 characters).

Protects against doing it twice if you retry: a retry with the same key gets the first answer back instead of running again.

Test mode (dry run): nothing will change
Turns test mode off for this page only. It switches back when you leave the page.

Code and response

curl -X POST 'https://mcp.wa-api.cloud/v1/saved-filters?dry_run=true' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
  "name": "Dubai VIPs",
  "filter": {
    "attributes": {
      "some": {
        "attributeName": {
          "eq": "city"
        },
        "stringValue": {
          "eq": "Dubai"
        }
      }
    }
  }
}'

The code reads your key from $API_KEY.

Parameters

Parameters
FieldTypeWhat it is
countboolean · queryWith dry_run=true: also count the contacts it would match.

Body

Body fields
FieldTypeWhat it is
namerequiredstringName shown in the app (1–255 characters).1–255 characters
descriptionstringOptional description (up to 1000 characters).0–1000 characters
filterrequiredobjectThe filter to save (the Search contacts format).

Headers

Headers
FieldTypeWhat it is
AuthorizationrequiredheaderBearer $API_KEY — your API key.
Api-VersionheaderThe API version to use, e.g. 2026-10-01. Default: the version your key is pinned to.one of: 2026-10-01 · pattern ^\d{4}-\d{2}-\d{2}$
Idempotency-KeyheaderAny unique string (8–128 characters). A retry with the same key returns the first answer instead of running twice. Kept 24 hours.pattern ^[A-Za-z0-9._:-]+$ · 8–128 characters

Response 201

The saved filter (with `dry_run=true`: as it would be saved, `id: null`).

Response fields
FieldTypeWhat it is
saved_filterrequiredobject
idrequiredstring or nullnull in a dry run: known only when it is saved.
namerequiredstring
descriptionrequiredstring or null
owner_staff_idrequiredstring or null
filterrequiredobjectThe definition as stored: the panel format, dates absolute.
updated_atrequiredstring or nullnull in a dry run.
resolved_datesrequiredarray of object
pathrequiredstring
inputrequiredstring
valuerequiredstring
warningsrequiredarray of objectFilter conditions that may not mean what they look like: {path, message}.
pathstring
messagerequiredstring
dry_runbooleantrue when this was a dry run: every check ran and nothing changed.
countinteger or nullWith dryRun + count: contacts it would match now (null when the count failed: see countError).-9007199254740991–9007199254740991
approximateboolean or nullWith count: the service counted approximately.
count_errorobjectWhy the count is missing. The preview itself is still valid.
coderequiredstring
messagerequiredstring

Errors

Errors are application/problem+json. Branch on code.

StatusCodeWhen
400invalid_input

A field is missing or has the wrong format. errors[] points at each field. The filter is not valid (each problem has its field, and reason names the class when there is one, as for Search contacts), it is larger than 32 KB, or count=true without dry_run=true.

401unauthenticated

The Authorization header is missing, the key is unknown, expired or revoked.

403entitlement_required

The workspace's plan does not include API access (api_access).

403forbidden

The filter compares phone numbers, which are hidden from this staff member (reason: phone_hidden).

403insufficient_scope

The key does not have the permission this operation needs.

404not_found

A saved filter it names does not exist in your workspace (reason: saved_filter_not_found).

409conflict

Also returned while a request with the same Idempotency-Key is still running.

429rate_limited

The key or workspace went over its rate limit. Wait for Retry-After seconds.

503upstream_unavailable

A service behind the API is briefly unavailable. Safe to retry with backoff.

504timeout

The change did not finish in time. Retry with the same Idempotency-Key: it never runs twice.

Examples

Save "Dubai VIPs"

Request body

{
  "name": "Dubai VIPs",
  "filter": {
    "attributes": {
      "some": {
        "attributeName": {
          "eq": "city"
        },
        "stringValue": {
          "eq": "Dubai"
        }
      }
    }
  }
}

Response 201

{
  "saved_filter": {
    "id": "7",
    "name": "Dubai VIPs",
    "description": null,
    "owner_staff_id": "501",
    "filter": {
      "attributes": {
        "some": {
          "attributeName": {
            "eq": "city"
          },
          "stringValue": {
            "eq": "Dubai"
          }
        }
      }
    },
    "updated_at": "2026-09-30T08:00:00.000Z"
  },
  "resolved_dates": [],
  "warnings": []
}
Preview a save and count its matches (dry run)

Request body

{
  "name": "New this month",
  "filter": {
    "createdAt": {
      "gte": "start_of_month"
    }
  }
}

Response 201

{
  "saved_filter": {
    "id": null,
    "name": "New this month",
    "description": null,
    "owner_staff_id": "501",
    "filter": {
      "createdAt": {
        "gte": "2026-08-31T20:00:00.000Z"
      }
    },
    "updated_at": null
  },
  "resolved_dates": [
    {
      "path": "filter.createdAt.gte",
      "input": "start_of_month",
      "value": "2026-08-31T20:00:00.000Z"
    }
  ],
  "warnings": [],
  "dry_run": true,
  "count": 120,
  "approximate": false
}

Operation path

The same operation is also at POST /v1/ops/crm_create_saved_filter, with every field in the JSON body.