{
  "openapi": "3.0.3",
  "info": {
    "title": "WalkLists API",
    "version": "1.0.0",
    "description": "The WalkLists public API. Read and write the contacts, lists, campaigns and\ndoor-level activity behind your field operation, so WalkLists can sit inside\nyour existing stack instead of beside it.\n\n## Authentication\n\nEvery request needs an API key. Send it either way:\n\n- Header: `Authorization: Bearer wl_live_...`  *(preferred)*\n- Query parameter: `?api_key=wl_live_...`  *(for tools that cannot set headers — spreadsheets, some no-code builders)*\n\nCreate and rotate keys at **Account → API keys**. The key is shown once, at\ncreation. We store only a hash, so a lost key is rotated, never recovered.\n\nKeys are scoped to an **organization**, not a user. A key keeps working when\nthe person who created it leaves.\n\n## Plans\n\nAPI access is included from the Pro tier upward. Limits are per organization:\n\n| Plan                              | API      | Scopes            | Requests/min | Requests/day |\n|-----------------------------------|----------|-------------------|--------------|--------------|\n| Starter (free)                    | —        | —                 | —            | —            |\n| Team / Local                      | Yes      | read              | 60           | 1,000        |\n| Pro / Campaign                    | Yes      | read, write       | 300          | 25,000       |\n| Business / Organization / Party / Enterprise | Yes | read, write, admin | 1,000    | unmetered    |\n\nA key may be narrowed to fewer scopes than the plan allows; it can never be\nwidened beyond it. Requesting a scope above your plan is rejected at creation\nrather than silently trimmed.\n\n## Rate limits\n\nEvery response carries:\n\n- `X-RateLimit-Limit` — requests allowed this minute\n- `X-RateLimit-Remaining` — requests left in the current window\n- `X-RateLimit-Reset` — Unix timestamp when the window resets\n- `X-API-Plan` — the plan tier this key resolved to\n\nOn `429`, wait for `Retry-After` seconds.\n\n## Pagination\n\nCollection endpoints return a `data` array plus a `meta` object with\n`page`, `per_page`, `total` and `has_more`. Ask for the next page with\n`?page=2`.\n\n## Syncing (polling integrations)\n\nFor one-way sync into a CRM, filter with `updated_since` and sort by\n`updated_at`. That is the pattern the Zapier connector uses, and it is\nstable across deletes, unlike page offsets.\n\n## Webhooks\n\nRather than polling, you can have WalkLists POST events to you as they\nhappen. Add an endpoint under **Account → Webhooks**; the signing secret is\nshown once, at that moment.\n\n### What we send\n\nA `POST` with `Content-Type: application/json` and this body:\n\n```json\n{\n  \"event\": \"activity.logged\",\n  \"created_at\": \"2026-07-21T17:54:48+00:00\",\n  \"data\": { \"activity\": { \"id\": 42, \"disposition\": \"not_home\" } }\n}\n```\n\nHeaders on every delivery:\n\n| Header | Meaning |\n|---|---|\n| `X-WalkLists-Signature` | `t=<unix-ts>,v1=<hmac-sha256>` — see below |\n| `X-WalkLists-Event` | The event name, so you can route without parsing the body |\n| `X-WalkLists-Attempt` | 1 on the first try, higher on retries |\n| `X-WalkLists-Delivery-Id` | Stable per delivery — use it to make your handler idempotent |\n\n### Verifying the signature\n\n**Verify before you trust the body.** Anyone can POST to your URL; the\nsignature is what proves the payload came from us.\n\n1. Read `t` and `v1` from the `X-WalkLists-Signature` header.\n2. Compute `HMAC-SHA256(secret, t + \".\" + rawRequestBody)`.\n   Use the **raw** body — re-serialising the JSON changes the bytes and the\n   signature will not match.\n3. Compare against `v1` with a **timing-safe** comparison\n   (`hash_equals`, `crypto.timingSafeEqual`) — a plain `==` leaks how much\n   of the digest matched.\n4. Reject if `t` is more than **300 seconds** from now. Without this check a\n   captured request can be replayed against you forever.\n\n```php\n[$t, $v1] = [null, null];\nforeach (explode(',', $header) as $part) {\n    [$k, $v] = explode('=', trim($part), 2);\n    if ($k === 't')  { $t = (int) $v; }\n    if ($k === 'v1') { $v1 = $v; }\n}\n$expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);\n$valid = hash_equals($expected, $v1) && abs(time() - $t) <= 300;\n```\n\n### Responding, retries, and failure\n\nReturn any `2xx` as soon as you have stored the payload. Do the slow work\nafterwards — we time out at 10 seconds.\n\nA non-2xx or a timeout is retried **5 times** with increasing delays:\n30 seconds, 2 minutes, 10 minutes, 30 minutes, then 1 hour. After that the\ndelivery is marked exhausted.\n\nAfter **15 consecutive failed deliveries** the endpoint is disabled and we\nstop sending. Fix the receiver and press **Resume** in the UI — that clears\nthe failure count.\n\nBecause retries exist, your handler must be **idempotent**: the same\n`X-WalkLists-Delivery-Id` can legitimately arrive more than once.\n\n### Events\n\n| Event | Fires when | Payload |\n|---|---|---|\n| `contact.created` | A contact is created — through the API **or** added in the dashboard | `data.contact`, a `Contact` |\n| `contact.updated` | A contact is edited — through the API **or** in the dashboard, including an address fix that re-runs geocoding | `data.contact`, a `Contact` |\n| `activity.logged` | A door is knocked in the field — disposition, notes, location | `data.activity`, an `Activity` |\n| `list.created` | A contact list is created, including by CSV import | `data.list` |\n| `ping` | Sent only by the **Test** button | — |\n\nThe object inside `data` is the **same shape** the corresponding endpoint\nreturns, so a payload can be handled by the same code that handles a\n`GET /contacts` or `GET /activity` row. In particular `activity.logged`\ncarries `contact_ref`, not `contact_id` — see the `Activity` schema for why\nthat distinction matters.\n\n> `campaign.created` is accepted by the events filter but is **not emitted\n> yet**. Subscribing to it is harmless; nothing will arrive until campaign\n> creation starts emitting it.\n\nBulk operations — CSV import and paper scan-back — deliberately do **not**\nemit one event per row. A 25,000-row import would otherwise mean 25,000\nrequests to your server.\n\n## Errors\n\nErrors share one shape, so a client can handle them generically:\n\n```json\n{ \"error\": { \"code\": \"invalid_api_key\", \"message\": \"Invalid API key.\", \"status\": 401 } }\n```\n",
    "contact": {
      "name": "WalkLists",
      "url": "https://walklists.com"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://walklists.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://walklists.com/api/v1",
      "description": "Production"
    }
  ],
  "security": [
    {
      "ApiKeyHeader": []
    },
    {
      "ApiKeyQuery": []
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Verify a key and read the plan it resolves to."
    },
    {
      "name": "Contacts",
      "description": "The people and doors in your organization."
    },
    {
      "name": "Lists",
      "description": "Imported or filtered groups of contacts."
    },
    {
      "name": "Campaigns",
      "description": "Field campaigns that organise the work."
    },
    {
      "name": "Activity",
      "description": "Door-level results — knocks, dispositions, notes."
    },
    {
      "name": "Hooks",
      "description": "REST-hook subscriptions. Subscribe a URL to an event and we POST to it as\nthe event happens, instead of you polling for changes.\n"
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "tags": [
          "Account"
        ],
        "summary": "Verify a key",
        "description": "Returns the organization and plan a key resolves to. Use it as a\nconnection test — it is the endpoint integrations call to validate\ncredentials before doing anything else.\n",
        "operationId": "getMe",
        "responses": {
          "200": {
            "description": "The key is valid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanForbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/contacts": {
      "get": {
        "tags": [
          "Contacts"
        ],
        "summary": "List contacts",
        "operationId": "listContacts",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          },
          {
            "$ref": "#/components/parameters/UpdatedSince"
          },
          {
            "$ref": "#/components/parameters/Sort"
          },
          {
            "name": "list_id",
            "in": "query",
            "description": "Only contacts belonging to this list.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Free-text match on name, email, phone or address.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of contacts.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Contact"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanForbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "post": {
        "tags": [
          "Contacts"
        ],
        "summary": "Create a contact",
        "description": "Requires the `write` scope.",
        "operationId": "createContact",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The created contact.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Contact"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ScopeForbidden"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/contacts/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Contacts"
        ],
        "summary": "Get a contact",
        "operationId": "getContact",
        "responses": {
          "200": {
            "description": "The contact.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Contact"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      },
      "patch": {
        "tags": [
          "Contacts"
        ],
        "summary": "Update a contact",
        "description": "Requires the `write` scope. Only the fields you send are changed.",
        "operationId": "updateContact",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ContactInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated contact.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Contact"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/ScopeForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/lists": {
      "get": {
        "tags": [
          "Lists"
        ],
        "summary": "List contact lists",
        "operationId": "listLists",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          },
          {
            "$ref": "#/components/parameters/Sort"
          }
        ],
        "responses": {
          "200": {
            "description": "A page of lists.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ContactList"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/lists/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Lists"
        ],
        "summary": "Get a list",
        "operationId": "getList",
        "responses": {
          "200": {
            "description": "The list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/ContactList"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/campaigns": {
      "get": {
        "tags": [
          "Campaigns"
        ],
        "summary": "List campaigns",
        "operationId": "listCampaigns",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          },
          {
            "$ref": "#/components/parameters/Sort"
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of campaigns.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Campaign"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/campaigns/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "integer"
          }
        }
      ],
      "get": {
        "tags": [
          "Campaigns"
        ],
        "summary": "Get a campaign",
        "operationId": "getCampaign",
        "responses": {
          "200": {
            "description": "The campaign.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Campaign"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    },
    "/activity": {
      "get": {
        "tags": [
          "Activity"
        ],
        "summary": "List door activity",
        "description": "Knocks, dispositions and notes recorded in the field, newest first.\nThis is the endpoint most integrations poll: filter on `updated_since`\nto push new results into a CRM.\n",
        "operationId": "listActivity",
        "parameters": [
          {
            "$ref": "#/components/parameters/Page"
          },
          {
            "$ref": "#/components/parameters/PerPage"
          },
          {
            "$ref": "#/components/parameters/UpdatedSince"
          },
          {
            "$ref": "#/components/parameters/Sort"
          },
          {
            "name": "campaign_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "contact_ref",
            "in": "query",
            "description": "Filter to one door, using the contact_ref returned on an activity.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "disposition",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of activity.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Activity"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PageMeta"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/hooks": {
      "post": {
        "tags": [
          "Hooks"
        ],
        "summary": "Subscribe to an event",
        "operationId": "createHook",
        "description": "Register a URL to receive one event type as it happens. This is what\npowers the WalkLists Zapier app, and it is available to any plan with\nthe `read` scope — a subscription only ever sends data out.\n\nSubscribing is idempotent for a given URL and event: calling it again\nreturns the existing subscription rather than creating a second one, so\na retried request cannot make your endpoint fire twice per event.\n\nThe delivered body is the same signed envelope documented under\nWebhooks. If your endpoint answers `410 Gone`, we treat the\nsubscription as cancelled and remove it.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url",
                  "event"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 2048,
                    "description": "HTTPS URL to deliver to. Must resolve to a public address.",
                    "example": "https://hooks.zapier.com/hooks/standard/123456/abcdef/"
                  },
                  "event": {
                    "type": "string",
                    "description": "The single event this subscription receives.",
                    "enum": [
                      "contact.created",
                      "contact.updated",
                      "activity.logged",
                      "campaign.created",
                      "list.created"
                    ],
                    "example": "contact.created"
                  },
                  "external_id": {
                    "type": "string",
                    "maxLength": 191,
                    "description": "Your own identifier for this subscription, echoed back for support."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The subscription. Returns the existing one if it already existed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Hook"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/ValidationFailed"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanForbidden"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          }
        }
      }
    },
    "/hooks/{id}": {
      "delete": {
        "tags": [
          "Hooks"
        ],
        "summary": "Unsubscribe",
        "operationId": "deleteHook",
        "description": "Remove a subscription. Succeeds whether or not it still exists, so\ntearing down twice is safe.\n\nOnly subscriptions created through this endpoint can be removed here.\nWebhooks you created in the WalkLists dashboard are managed there.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Removed, or already absent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer",
                          "example": 42
                        },
                        "deleted": {
                          "type": "boolean",
                          "example": true
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/PlanForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyHeader": {
        "type": "http",
        "scheme": "bearer",
        "description": "Send the key as `Authorization: Bearer wl_live_...`."
      },
      "ApiKeyQuery": {
        "type": "apiKey",
        "in": "query",
        "name": "api_key",
        "description": "For clients that cannot set headers."
      }
    },
    "parameters": {
      "Page": {
        "name": "page",
        "in": "query",
        "description": "1-based page number.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "default": 1
        }
      },
      "PerPage": {
        "name": "per_page",
        "in": "query",
        "description": "Items per page.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "maximum": 200,
          "default": 50
        }
      },
      "UpdatedSince": {
        "name": "updated_since",
        "in": "query",
        "description": "Only records changed at or after this instant (ISO 8601). The reliable\nway to poll for changes — unlike page offsets, it is not disturbed by\ninserts or deletes between calls.\n",
        "schema": {
          "type": "string",
          "format": "date-time"
        }
      },
      "Sort": {
        "name": "sort",
        "in": "query",
        "description": "Sort order. Prefix with `-` for descending, e.g. `sort=-created_at`.\n\nThe default is ascending `updated_at`, which is what you want when\nwalking a change feed with `updated_since`. Use `-created_at` when you\nwant the newest records first — that is the order a poller needs, since\nit reads only the first page.\n",
        "schema": {
          "type": "string",
          "enum": [
            "created_at",
            "-created_at",
            "updated_at",
            "-updated_at",
            "id",
            "-id"
          ]
        }
      }
    },
    "schemas": {
      "PageMeta": {
        "type": "object",
        "properties": {
          "page": {
            "type": "integer",
            "example": 1
          },
          "per_page": {
            "type": "integer",
            "example": 50
          },
          "total": {
            "type": "integer",
            "example": 1284
          },
          "has_more": {
            "type": "boolean",
            "example": true
          }
        }
      },
      "Account": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "The key's own name.",
            "example": "Zapier integration"
          },
          "plan": {
            "type": "string",
            "example": "professional"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "revoked"
            ]
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "read",
                "write",
                "admin"
              ]
            }
          },
          "rate_limit": {
            "type": "integer",
            "example": 300
          },
          "daily_limit": {
            "type": "integer",
            "description": "0 means unmetered.",
            "example": 25000
          },
          "usage": {
            "type": "object",
            "description": "Your organization's consumption, so an integration can pace itself instead of discovering the ceiling by being 429'd mid-sync.",
            "properties": {
              "today": {
                "type": "integer",
                "example": 412
              },
              "this_month": {
                "type": "integer",
                "example": 9310
              }
            }
          },
          "organization": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              }
            }
          },
          "key": {
            "type": "object",
            "properties": {
              "hint": {
                "type": "string",
                "description": "Last 4 characters, for identification.",
                "example": "a1b2"
              },
              "type": {
                "type": "string",
                "enum": [
                  "production",
                  "test"
                ]
              },
              "last_used_at": {
                "type": "string",
                "format": "date-time",
                "nullable": true
              }
            }
          }
        }
      },
      "Contact": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "first_name": {
            "type": "string",
            "nullable": true
          },
          "last_name": {
            "type": "string",
            "nullable": true
          },
          "email": {
            "type": "string",
            "nullable": true
          },
          "phone": {
            "type": "string",
            "nullable": true
          },
          "mobile_phone": {
            "type": "string",
            "nullable": true
          },
          "address": {
            "type": "string",
            "nullable": true
          },
          "address_2": {
            "type": "string",
            "nullable": true
          },
          "city": {
            "type": "string",
            "nullable": true
          },
          "state": {
            "type": "string",
            "nullable": true
          },
          "zip": {
            "type": "string",
            "nullable": true
          },
          "country": {
            "type": "string",
            "nullable": true
          },
          "latitude": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "longitude": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "geocode_status": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "nullable": true
          },
          "source": {
            "type": "string",
            "nullable": true
          },
          "homeowner": {
            "type": "boolean",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "string"
            }
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "ContactInput": {
        "type": "object",
        "description": "Only these fields are writable through the API. Geocoding fields are\nderived by WalkLists and cannot be set directly — send an address and\nwe resolve it.\n",
        "properties": {
          "first_name": {
            "type": "string",
            "maxLength": 255
          },
          "last_name": {
            "type": "string",
            "maxLength": 255
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 255
          },
          "phone": {
            "type": "string",
            "maxLength": 255
          },
          "mobile_phone": {
            "type": "string",
            "maxLength": 255
          },
          "address": {
            "type": "string",
            "maxLength": 255
          },
          "address_2": {
            "type": "string",
            "maxLength": 255
          },
          "city": {
            "type": "string",
            "maxLength": 255
          },
          "state": {
            "type": "string",
            "maxLength": 255
          },
          "zip": {
            "type": "string",
            "maxLength": 255
          },
          "country": {
            "type": "string",
            "maxLength": 255
          },
          "status": {
            "type": "string",
            "maxLength": 50
          },
          "source": {
            "type": "string",
            "maxLength": 255
          },
          "homeowner": {
            "type": "boolean"
          },
          "notes": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ContactList": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "nullable": true
          },
          "contact_count": {
            "type": "integer",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "Campaign": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "name": {
            "type": "string"
          },
          "vertical": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "nullable": true
          },
          "start_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "end_date": {
            "type": "string",
            "format": "date",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "Activity": {
        "type": "object",
        "description": "A door result recorded in the field — the disposition, any survey answers, and where it was logged.",
        "properties": {
          "id": {
            "type": "integer"
          },
          "contact_ref": {
            "type": "integer",
            "nullable": true,
            "description": "The field view's own stop reference for this door. It is NOT a /contacts id — the two id spaces do not overlap, so do not try to look it up there. It is stable within a campaign."
          },
          "campaign_id": {
            "type": "integer",
            "nullable": true
          },
          "disposition": {
            "type": "string",
            "nullable": true,
            "description": "The outcome at the door, as recorded by the rep.",
            "example": "not_home"
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "answers": {
            "type": "object",
            "nullable": true,
            "description": "Survey answers for this door. The shape is defined by the campaign's own script, so treat it as free-form.",
            "additionalProperties": true
          },
          "latitude": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "longitude": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "updated_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        }
      },
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "status": {
                "type": "integer"
              }
            }
          }
        }
      },
      "Hook": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer",
            "example": 42
          },
          "url": {
            "type": "string",
            "format": "uri",
            "example": "https://hooks.zapier.com/hooks/standard/123456/abcdef/"
          },
          "event": {
            "type": "string",
            "example": "contact.created"
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "paused",
              "disabled"
            ],
            "example": "active"
          },
          "source": {
            "type": "string",
            "enum": [
              "manual",
              "zapier"
            ],
            "example": "zapier"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing, unrecognised or revoked key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "missing": {
                "value": {
                  "error": {
                    "code": "missing_api_key",
                    "message": "API key required. Send it as an 'api_key' query parameter or an 'Authorization: Bearer <key>' header.",
                    "status": 401
                  }
                }
              },
              "revoked": {
                "value": {
                  "error": {
                    "code": "api_key_revoked",
                    "message": "This API key has been revoked.",
                    "status": 401
                  }
                }
              }
            }
          }
        }
      },
      "PlanForbidden": {
        "description": "The organization's plan does not include API access.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "api_not_available_on_plan",
                "message": "The API is not included in your current plan. Upgrade to Pro or above to enable API access.",
                "status": 403
              }
            }
          }
        }
      },
      "ScopeForbidden": {
        "description": "The plan or the key lacks the scope this endpoint needs.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "plan_scope_denied",
                "message": "Your plan does not include 'write' API access.",
                "status": 403
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "No such record in your organization.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "not_found",
                "message": "Not found.",
                "status": 404
              }
            }
          }
        }
      },
      "ValidationFailed": {
        "description": "The request body failed validation.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "Too many requests. Retry after the window resets.",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait."
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": {
                "code": "rate_limit_exceeded",
                "message": "Rate limit exceeded. Slow down and retry.",
                "status": 429
              }
            }
          }
        }
      }
    }
  },
  "x-source-sha256": "70de4dcfa006d46dbea7bbbd141198cd1e04a48f3fb6adf8e9f48f064178fa49",
  "x-generated-by": "docs/api/build-spec.js"
}
