> **Correction, 2026-09-09.** This report stated that Aptly publishes no
> plan tiers or prices. That was wrong. getaptly.com/pricing publishes three
> tiers, Essential, Premium and Enterprise, with prices, and lists "Access to
> Aptly API" as a Premium plan feature. The language below is corrected.
> **The score is unchanged at 58 (F).** C5.3 was never scored on the pricing
> page: it rests on Aptly's Help Center stating the API requires a Premium
> Subscription, which the pricing page now independently corroborates.

# API Report Card: Aptly Core API

## Run metadata
- Methodology version: 1.1
- Evaluating model: Claude Opus 5 (claude-opus-5)
- Date run: 2026-09-03
- Provisional evidence-packet version or ID: `aptly-ep-2026-09-03-p1` (frozen after initial discovery, before check marks)
- Final evidence-packet version or ID: `aptly-ep-2026-09-03-final` (frozen after the controlled verification pass)
- Evidence-discovery mode: tool-enabled discovery
- Evidence tier: **Fully verified — controlled live**
- Live-write method and safety: controlled live — one labeled fixture card (`APITEST-DELETE 2026-09-03 api-grader`) created on the only API-enabled board with zero automations and zero workflows; operator authorization recorded in-session on 2026-09-03; containment verified across all four API-enabled boards; fixture archived and the board's active card count restored to its pre-test baseline of 190
- Minimum live-test battery: **complete** — steps 1–6 run; step 7 (idempotency) N-A because the API documents no idempotency mechanism; step 8 (webhooks) N-A because no registrable webhook or event subscription was evidenced
- Live tests performed: authenticate; credential introspection; read and page a core collection (190 cards, stable ordering verified across repeated and adjacent pages); incremental `updatedAtMin` query at five time offsets; documented-filter probe (`keyTerm`); ten deliberate error probes; response-header and rate-limit-signal capture; 255-request concurrent burst; conditional GET (`If-None-Match`); create a card; read it back; partial-update a field; stale-`If-Match` concurrency probe; stage transition; archive; `DELETE` probe; cross-board containment scan
- Live tests not possible: none
- Documentation-graded checks (baseline verified): none. Two write-adjacent checks are graded from documented absence rather than observation: C2.4 (no idempotency mechanism exists to test) and C1.3's assignment sub-item (assignee writes documented in the spec; not exercised because assignment on a real board can notify a real person and is on the protocol's hard exclusion list)

## Final evidence packet manifest
First-party documentation and product surfaces:
- https://docs.getaptly.com/llms.txt — complete documentation index, 89 entries (accessed 2026-09-03)
- https://docs.getaptly.com/llms-full.txt — full machine-readable documentation corpus, 250,953 bytes
- https://docs.getaptly.com/openapi.yaml — Aptly API OpenAPI 3.0.3 specification, 157,565 bytes, 53 paths
- https://docs.getaptly.com/portal/openapi.yaml — Aptly Portal API specification, 14,341 bytes
- https://docs.getaptly.com/api-reference/openapi.json — **not an Aptly artifact**; returns the Mintlify sample "OpenAPI Plant Store" spec (5,173 bytes)
- https://docs.getaptly.com/introduction.md — data model, base URL, capabilities
- https://docs.getaptly.com/authentication.md — API keys, `x-token` header, error-status table, key expiration
- https://docs.getaptly.com/pagination.md — `page` / `limit` parameters, last-page detection
- https://docs.getaptly.com/rate-limits.md — 120 req/min per key, 20 req/s burst, 429 + `Retry-After`, backoff sample
- https://docs.getaptly.com/field-types.md — field-type value formats
- https://docs.getaptly.com/mcp-server.md — MCP tool catalog with scopes and throttles (46,944 bytes)
- https://docs.getaptly.com/changelog.md — dated API changelog, most recent entry 2026-08-24
- https://docs.getaptly.com/delegate-tokens.md — short-lived JWT issuance
- https://docs.getaptly.com/quickstart.md
- https://docs.getaptly.com/api-reference/cards/create-or-update-a-card.md — representative endpoint page
- https://docs.getaptly.com/api-reference/boards/list-boards.md, .../cards/list-cards.md, .../contacts/list-contacts.md, .../tasks/search-tasks.md, .../board/get-full-board-configuration.md, .../board/add-a-tab-view-legacy.md
- https://www.getaptly.com/ — primary navigation, Developer Portal link
- https://www.getaptly.com/platform/api-mcp — API & MCP product page
- https://www.getaptly.com/pricing — three published tiers (Essential, Premium, Enterprise) with prices; "Access to Aptly API" listed as a Premium plan feature [rechecked live 2026-09-09]
- https://aptly.helpkit.so/integrations/vVQpukZPcgx25GULuxrwjv/connect-to-aptly's-api/fPZrn228WQW331GV7rGLct — Aptly Help Center, "Connect to Aptly's API", last updated 2026-05-26 (raw page captured, 369,402 bytes)
- https://getaptly.com/api — returns HTTP 404 (linked from search results; not retrievable)
- https://status.getaptly.com/ — resolves via Cloudflare but serves an unterminated 301 redirect loop; no status content retrievable at `/`, `/history`, or `/api/v2/status.json`

Live observations (production account "[operator account]", company `[account id withheld]`, operator-supplied API key, all 2026-09-03):
- 99 individually captured calls (read and write) plus a 255-request concurrent burst against `https://core-api.getaptly.com`; full request/response/header captures retained per call
- Boards observed: `TMpHNWjuesh3NY9bh` Owner Leads (56 cards), `aHjNe2aLBFDhbcpPR` Investor Plans (81), `lease` Leases (190), `rctFrWrnsfE6HQYYr` Lease Renewals (38)

## Evidence-amendment log
Sources added during the controlled verification pass, and the checks they affect:
- **C5.3** — https://aptly.helpkit.so/.../connect-to-aptly's-api/... added. First-party Help Center states the API "does require a Premium Subscription." Changed C5.3 from a provisional `yes` (no tier gating evidenced in the developer portal) to `no`.
- **C1.4, C2.8** — same Help Center page added. It contains a "Webhooks" table whose rows are, with one exception, plainly inbound API endpoints (`getCards`, `getSchema`, `getCard`, `postComment`, `postFile`, `getContacts`, `postContact`, `getUsers`), and the same page states the API "only supports creating cards on a board." One row, `addCard` — "Triggered when a new card is added / Sends a notification to inform the receiving service that a card was added to the board" — reads as an outbound event. No registration mechanism, event catalog, signature scheme, or retry policy is documented anywhere, and the board-configuration endpoint (documented as returning *all* configuration sections) exposes no webhook section. C1.4 held at `partial` on incremental polling; C2.8 changed from a provisional `N-A` to `unverified`.
- **C2.12** — https://status.getaptly.com/ probed at four paths plus DNS. Hostname resolves to Cloudflare and returns an unterminated 301 loop. No SLA or uptime language appears anywhere in the 250,953-byte documentation corpus. Held at `no`.
- **C2.10** — `openapi.yaml` grepped for a request-level version identifier (`/v1`, `api-version`, `X-Api-Version`, `Accept-Version`): none. Documentation corpus grepped for deprecation-window or backward-compatibility policy language: none. Changed C2.10 from a provisional `partial` to `no`.
- **C3.1, C3.2** — changelog entry of 2026-08-13 added, establishing per-board and per-permission key scoping. Raised C3.1 and C3.2 from provisional `unverified` to `yes`.

## API eligibility
- Qualifying API: **yes**
- API operator: Aptly — `servers: - url: https://core-api.getaptly.com / description: Production` in `openapi.yaml`, served from Aptly's own documentation domain
- Access or credential issuer: Aptly, self-serve inside the product — "Open the board in Aptly → Card Sources → API → Toggle the API on → Create New Key" (`authentication.md`, "API keys")
- Eligibility basis: A first-party OpenAPI 3.0.3 specification on Aptly's documentation domain defines 53 paths against `core-api.getaptly.com` exposing Aptly's own boards, cards, contacts, tasks, inboxes, templates, knowledge documents, and files. Live-verified: `GET /api/app/me` with the operator's key returned `{"type":"apiKey","companyId":"[account id withheld]","companyName":"[operator account]"}` (2026-09-03), confirming the interface serves Aptly's own functions under an Aptly-issued credential.

## Context
- Software category: **workflow / CRM tool**
- What the API is for and its core objects and workflows: Aptly is an operations layer that sits on top of a property management system rather than replacing it, and its API is a generic board-and-card workflow surface — "This API gives external tools, scripts, and AI agents direct read/write access to your boards, cards, contacts, and more" (`introduction.md`). Its core objects are boards (workflow workspaces), cards (records on a board, with dynamic customer-defined fields keyed by field UUID), contacts (company-scoped person records), automations and workflows (board-level rules that move cards and fire actions), plus tasks, templates, inboxes, and knowledge documents. Its core workflows are reading and writing card records, and firing board workflows by changing a card's stage.

## Provider and property-management fit
- What this product is: an AI-powered operations and CRM layer for property management companies that "integrates with your existing property management software and becomes the AI-powered operations layer on top" — https://www.getaptly.com/ and https://www.getaptly.com/platform/api-mcp, which names "Yardi, RealPage, Entrata, AppFolio, and more"
- Bank status, when relevant: **N-A** — no first-party evidence of any banking, deposit-taking, or fund-holding function
- Who provides any bank account or regulated banking service: **none** — no account, balance, payment-initiation, or settlement endpoint exists in either the Core API (`openapi.yaml`, 53 paths) or the Portal API specification
- What the customer actually receives: a software subscription to a hosted board/card workflow platform with email and phone inboxes, automations, an applicant screening portal, and a documented HTTP API and MCP server. No funds, ledger, or account relationship is involved.
- Property-management fit: **PM-specialized** — "Aptly is a property management platform" (`introduction.md`, opening line), with property-management workflows as the product's central purpose across https://www.getaptly.com/platform/leasing, /platform/ai-agents, and /platform/aptly-access
- Documented PM-specific workflows: leasing and applicant screening (`portal/openapi.yaml` — property listings, location search, nearby schools, application forms); board typing that includes a `screening` board type (`mcp-server.md`, `search-boards-by-name`); Rentvine, AppFolio, and Yardi integration (Help Center integration articles; https://www.getaptly.com/ai-property-management-software). Observed live on the operator's own account: four API-enabled PM boards — Owner Leads, Investor Plans, Leases, Lease Renewals — whose lease board carries 81 PM-domain fields including `startDate`, `endDate`, `moveInDate`, `increaseEligibilityDate`, `rent`, `depositBalance`, `overdueRentBalance`, and `rentersInsuranceExpirationDate`.
- Trust or fiduciary workflow support, when relevant: **not documented** — nothing in either specification addresses trust accounting, client funds, security-deposit escrow, or owner distributions. The lease board's balance fields (`depositBalance`, `overdueRentBalance`, `prepaymentBalance`) are customer-defined board fields synced in from a PMS, not ledger or trust primitives the API computes or controls.
- Operational role and dependencies: Aptly is the workflow and communication layer over your existing PMS, so an operator building on this API still needs their PMS of record (Rentvine, in this account's case) for leases, ledgers, work orders, and trust accounting, and a separate system for anything financial.

## Coverage classification (fixed before inspection)
Default Workflow/CRM classification, adopted without deviation. Fixed before any API inspection.

| Object or workflow | Class | Weight | Present / read-only / absent |
|---|---|---|---|
| **Objects** | | | |
| Records (cards, contacts, tasks, knowledge docs) | critical | 3 | Present with required operations |
| Automations / triggers | critical | 3 | Present but materially read-only |
| Boards / pipelines | important | 2 | Present but materially read-only |
| Custom fields | important | 2 | Present but materially read-only (values writable, definitions not) |
| Reporting / exports | optional | 1 | Present (computed — writes N-A within sub-map) |
| **Critical workflows** | | | |
| Create / update records | critical | 3 | Present |
| Fire and receive triggers | critical | 3 | Half present — fire yes, receive no |
| **Principal lifecycle changes** | | | |
| Stage / status change | critical | 3 | Present |
| Archive | important | 2 | Present |
| Delete | important | 2 | Absent |
| Assignment | important | 2 | Present (documented) |
| Task complete / uncheck | optional | 1 | Present |

## Functional coverage map
**Core objects.**
- *Records* — critical, w3, **1.0**. Cards: `GET /api/board/{boardId}` (list, live-verified 190 records), `GET /api/board/{boardId}/{cardId}` (single), `POST /api/board/{boardId}` (create and update, both live-verified). Contacts: `GET /api/contacts`, `POST /api/contacts` (upsert), `GET|POST /api/contacts/{contactId}`, `POST /api/contacts/by-email`. Tasks: `POST /api/tasks/search`, `POST /api/tasks`, `GET|POST /api/tasks/{taskId}`. Knowledge: `POST /api/knowledge/create`, `GET|PUT /api/knowledge/{id}`. Comments and files: `POST /api/board/{boardId}/{cardId}/comment`, `.../file`, `POST /api/files/upload-url`, `/upload-complete`. Full read and write across every record type.
- *Automations / triggers* — critical, w3, **0.5**. Readable: `GET /api/board/{boardId}/configuration/automations` and `.../workflows` (live-verified — 143 workflows on Owner Leads, 49 on Lease Renewals, 1 board automation each on Investor Plans and Lease Renewals). No create, update, or archive operation for an automation or workflow exists in the specification, and no mechanism exists to receive a trigger. Read-only where writes are operationally expected for a workflow tool.
- *Boards / pipelines* — important, w2, **0.5**. Readable: `GET /api/boards`, `GET /api/board/{boardId}/configuration` (all nine sections), `.../options`, `.../groups`, `.../shares`, `.../theme`, `.../filters`, `.../identity fields`. Writable: only `POST /api/board/{boardId}/configuration/tabViews` (attach an embedded tab). No board create or update.
- *Custom fields* — important, w2, **0.5**. Definitions readable via `GET /api/schema/{boardId}` (live-verified, 81 fields on the lease board) and `GET .../configuration/fields`. Field *values* are fully writable on cards (live-verified). Field *definitions* cannot be created or modified through the API.
- *Reporting / exports* — optional, w1, **1.0**. `GET /api/inboxes/{channelId}/analytics`, `/trends`, `/health`; `POST /api/tasks/search` with `useCount` (live-verified, returned `{"count":1000}`); full-dataset extraction via pagination (live-verified, 190 cards in one 620,429-byte response). Computed and generated outputs — writes marked N-A within this sub-map rather than counted as read-only.

Weighted object coverage = (3×1.0 + 3×0.5 + 2×0.5 + 2×0.5 + 1×1.0) ÷ 11 = 7.5 ÷ 11 = **68%**

**Primary operational workflows (mutable).**
- *Create / update records* — critical, w3, **1.0**. Live-verified 2026-09-03: `POST /api/board/lease` with `{"name":"APITEST-DELETE 2026-09-03 api-grader","rentvineId":"APITEST-DELETE"}` returned `200 {"data":{"_id":"dcfd4eea14874850bb85b125697eb788"}}`; board active count moved 190 → 191; read-back showed `createdConduit: "api"`. A follow-up `POST` with `{"_id":…,"rentersInsuranceCompany":"APITEST-DELETE-UPDATED"}` applied the change, preserved `name` and `rentvineId` (patch semantics as documented), and advanced `updatedAt` from `19:03:37.490Z` to `19:04:16.777Z`.
- *Fire and receive triggers* — critical, w3, **0.5**. Fire: live-verified — `POST /api/board/lease` with `{"_id":…,"stage":"Active"}` returned 200 and the read-back showed `stage: "Active"`, which is the documented trigger condition for board workflows (`triggerOn: "stage"` on all 49 Lease Renewals workflows). Receive: absent — no webhook, event, or subscription mechanism evidenced.
- *Board / pipeline writes* — important, w2, **0.5**. Tab views only.
- *Custom-field writes* — important, w2, **0.5**. Values yes, definitions no.

Weighted operational coverage = (3×1.0 + 3×0.5 + 2×0.5 + 2×0.5) ÷ 10 = 6.5 ÷ 10 = **65%**

**Principal lifecycle changes.**
- *Stage / status change* — critical, w3, **1.0**. Live-verified above.
- *Archive* — important, w2, **1.0**. Live-verified: `POST /api/board/lease` with `{"_id":…,"archived":true}` returned 200. Two immediate read-backs still reported `archived: false`; a read roughly two minutes later reported `archived: true`, active count 190, `includeArchived=true` count 191, and the card absent from active listings. The write applied; the reads were stale (see the read-after-write note in Category 2).
- *Delete* — important, w2, **0.0**. Live-verified absent: `DELETE /api/board/lease/{cardId}` returned `404 {"error":{"code":"NOT_FOUND","message":"Route not found"}}`. No `DELETE` verb appears on any of the 53 paths in `openapi.yaml`. `POST /api/routing-groups/{id}/archive` is the only dedicated retirement endpoint in the API, and it covers routing groups only.
- *Assignment* — important, w2, **1.0**. Documented: `assignee` is a writable card property (`openapi.yaml`, card `POST` body `additionalProperties`; `Card.assignee`), `PUT /api/tasks/{taskId}` accepts an assignee, and `mcp-server.md` documents `assign-user-to-card-task`. Not exercised live: assignment on a real board notifies a real person and is on the protocol's hard exclusion list.
- *Task complete / uncheck* — optional, w1, **1.0**. `PUT /api/tasks/{taskId}` with `checkedAt` (`api-reference/tasks/update-a-task.md`).

Weighted lifecycle coverage = (3×1.0 + 2×1.0 + 2×0.0 + 2×1.0 + 1×1.0) ÷ 10 = 8.0 ÷ 10 = **80%**

## Category 1: Functional Coverage and Usefulness: 7.5/15
- **C1.1 Object coverage: partial** — weighted coverage = 68% (0.50–0.84 band, no critical object absent). Records fully writable; automations/triggers, boards, and custom-field definitions are read-only. [`openapi.yaml` paths `/api/board/{boardId}`, `/api/contacts`, `/api/tasks`, `/api/knowledge/create`, `/api/board/{boardId}/configuration/automations`, `.../workflows`, `.../fields`; live: `GET /api/schema/lease` returned 81 field definitions with no write path, 2026-09-03]
- **C1.2 Core operational actions: partial** — weighted coverage = 65% (0.50–0.84 band, no critical write workflow wholly absent). Record create and update are live-verified and work exactly as documented, including patch semantics. Triggers can be fired but not received. [live: `POST /api/board/lease` create at 19:03:37Z, update at 19:04:16Z, stage transition at 19:05Z, 2026-09-03]
- **C1.3 Delete or lifecycle actions: partial** — weighted coverage = 80% (0.50–0.84 band, no critical lifecycle action absent). Stage change, archive, assignment, and task completion are all available; hard delete is absent from the entire API. [live: `DELETE /api/board/lease/{cardId}` → `404 NOT_FOUND` "Route not found", 2026-09-03; `openapi.yaml` contains no `delete:` operation on any of 53 paths]
- **C1.4 Change notification: partial** — no push mechanism, but efficient incremental polling is documented and works. `updatedAtMin` on the card-list endpoint is live-verified honored at hour granularity: `updatedAtMin` one hour back returned `count: 0`, one day back returned `count: 190`, and a 2030 timestamp returned `count: 0`; cards expose `updatedAt`, and the endpoint also filters on `relatedId`, `contactEmail`, `assignee`, and `includeArchived`. Two caveats. First, `updatedAtMin=notadate` returned `200` with a full unfiltered result set instead of a `400` — an invalid incremental cursor silently degrades to "fetch everything." Second, the documented `keyTerm` full-text filter returned `count: 0` for `APITEST` while a card named `APITEST-DELETE 2026-09-03 api-grader` existed on the board, and `count: 0` for `Riverside` on a 190-card Inland Empire lease board, so that filter is not dependable for change detection. [`openapi.yaml` `/api/board/{boardId}` parameters; `pagination.md`; live probes 10–13 and 24–28, 2026-09-03]

Score math: earned 2.0 of 4 applicable checks (4 × partial); unrounded fraction = 0.5000; category points = 0.5000 × 15 = **7.5/15**; verification coverage = 4/4 = **100%**

**What this means for you:** You can read everything on your Aptly boards and you can create and change cards reliably — I proved that on your own Leases board today. What you cannot do is manage the automation layer through the API, and you cannot be *notified* when something changes. Every integration you build has to poll Aptly on a timer and ask "what changed since last time," which is exactly what your renewal sync already does. That works, and the `updatedAtMin` filter is honest about hour-level changes, but be careful with two things: if your code ever sends a malformed timestamp Aptly will quietly hand back all 190 records instead of erroring, and the built-in text search does not reliably find cards that plainly exist. There is also no way to delete a card through the API — only archive — so any test or mistaken record has to be cleaned up by hand in Aptly.

## Category 2: API Design, Reliability, and Operability: 4.1/10
- **C2.1 Modern API conventions: partial** — resource-oriented JSON over HTTPS with a real OpenAPI 3.0.3 specification, standard status codes, and CORS headers, but conventions are mixed: `POST` doubles as create and update for cards (upsert keyed on an `_id` in the body) with no `PUT` or `PATCH`; no `DELETE` verb exists on any of the 53 paths; and several operations are RPC-shaped paths rather than resources — `POST /api/knowledge/create`, `POST /api/routing-groups/create`, `POST /api/routing-groups/{id}/archive`, `POST /api/tasks/search`. [`openapi.yaml` paths list; live: `DELETE` → 404 NOT_FOUND, 2026-09-03]
- **C2.2 Consistent typing: partial** — value typing is genuinely strong. Across all 190 live lease cards, every core type held: money is always `{"amount":2850,"currency":"USD"}`, dates are always ISO strings, `persons` and `relatedAptlets` are always arrays, `address` and `tel` are always objects. Three inconsistencies, cited exactly: (1) the OpenAPI `Error` schema declares `error` as `type: string` alongside a sibling `message`, but every live error returns `error` as a nested object — `{"error":{"code":"BOARD_NOT_FOUND","message":"Board not found"}}` — so the published schema for the core error contract is the wrong shape; (2) the list envelope is `{data, count, page, pageSize}`, the single-card envelope is `{data:{…}}`, and `GET /api/routing-groups` returns a bare JSON array with no envelope at all; (3) board field `68` ("Renewal RENT $") is declared `type: "string"` by `GET /api/schema/lease` but returns numbers (`2900`, `3195`, `2995`) — a customer-configured field, and the only value-level mismatch in 190 records. [`openapi.yaml` `components.schemas.Error` lines 585–591; live probes 03, 16, 36, 45, 04/08, 2026-09-03]
- **C2.3 Structured errors: partial** — the reported-error contract satisfies every element of a full pass: a consistent structured body, a populated and stable machine-readable code, a human-readable message, and correct HTTP status semantics. Ten deliberate probes returned `401 UNAUTHORIZED` (bad key and missing key), `404 BOARD_NOT_FOUND`, `404 CARD_NOT_FOUND`, `404 NOT_FOUND` (unknown route and unsupported verb), `400 INVALID_DATA` ("pageSize must be a number between 1 and 1000", "page must be a number between 0 and 9999"), and `400 MISSING_DATA` ("page parameter is required") — same shape every time. The limitation that holds this to partial: validation is not applied uniformly, so one class of failure returns no error at all. `updatedAtMin=notadate` returned `200` with 54,651 bytes of unfiltered cards, while an equally invalid `page=abc` on the same endpoint correctly returned `400`. A sync built on that parameter is told everything succeeded when its incremental cursor was discarded. [live probes 14–23, 2026-09-03]
- **C2.4 Duplicate prevention: no** — no idempotency mechanism is documented anywhere in the 250,953-byte documentation corpus or the specification: no idempotency key, no request-identifier deduplication, and no natural key on card create. `POST /api/board/{boardId}` deduplicates only when the caller already supplies an `_id`, which is unknowable before the first call succeeds, so a retried create after a timeout produces a second card. The one exception is narrow and not general-purpose: `POST /api/web-forms/{formId}` has a configurable "Update if this field matches" mode. Graded from documented absence; there was no mechanism to test. [`openapi.yaml` — no `Idempotency-Key` parameter on any path; `llms-full.txt` — zero occurrences of "idempot"]
- **C2.5 Graceful handling under load: yes** — `rate-limits.md` publishes explicit numeric limits (120 requests per minute per key, 20 per second burst), documents the `429` response body, states that "The response includes a `Retry-After` header indicating how many seconds to wait," and supplies a worked exponential-backoff implementation. Noted contradiction with observed behavior: 255 requests issued in about 1.1 seconds across two concurrent bursts (45 then 210) all returned `200`, and no rate-limit or quota headers appear on normal responses — so the documented ceiling did not engage and the `429` path itself was not observable. [`rate-limits.md`; live probes 53–54, 2026-09-03]
- **C2.6 Pagination for large collections: partial** — on cards, pagination is exemplary and live-verified: `page` plus `pageSize` (max 1000), a `count` total, stable ordering (page 0 returned an identical ID sequence on repeat, and pages 0 and 1 had zero overlap), and a clean `400 INVALID_DATA` above the cap. The limitation: `GET /api/contacts` ignores the page-size parameter entirely — both `?limit=3` and `?pageSize=3` returned 200 records and echoed `pageSize: 200` — so page size on a 2,112-record core collection is an undocumented fixed cap the caller cannot control. Compounding it, `pagination.md` documents the parameter as `limit`, default 50, maximum 100, which matches neither endpoint tested (cards use `pageSize` to 1000; contacts ignore both). Traversal still works — contact pages advanced with no overlap and a correct `count`. [`pagination.md`; live probes 05–09, 32, 48–52, 2026-09-03]
- **C2.7 Bulk or incremental export: yes** — documented incremental sync via updated-since plus pagination, which the check accepts as qualifying, and live-verified end to end: `updatedAtMin` is honored, and `pageSize=1000` returned the full 190-card dataset in a single 620,429-byte response with no per-record calls. `GET /api/docs/openapi` additionally serves the specification unauthenticated. [`openapi.yaml` `/api/board/{boardId}` parameters; `pagination.md` "Iterating all records"; live probes 08, 11, 24–28, 2026-09-03]
- **C2.8 Webhook security and delivery reliability: unverified** — cannot be established either way. Aptly's developer portal contains no webhook, event, or subscription content across its entire 250,953-byte corpus, and `GET /api/board/{boardId}/configuration` — documented as returning *all* configuration sections — returned nine sections (fields, automations, options, tabViews, workflows, groups, shares, theme, filters) with zero occurrences of "webhook", "callback", or "postback" in the full response. Against that, the Help Center page carries a "Webhooks" table whose `addCard` row reads "Triggered when a new card is added / Sends a notification to inform the receiving service that a card was added to the board", while the same page's other eight rows are plainly inbound API endpoints and the page states the API "only supports creating cards on a board." A capability is referenced but no signature scheme, retry policy, replay guidance, event catalog, or registration path is readable, so it can be neither credited nor ruled out. Resolving it does not change the published score (see Total). [`llms-full.txt`; live probe 37; Help Center page captured 2026-09-03, page last updated 2026-05-26]
- **C2.9 Concurrency and conflict control: no** — neither optimistic concurrency nor documented conflict semantics. Live-verified: `ETag` is returned on every read and conditional reads work (`If-None-Match` with a current ETag returned `304`), but an update sent with a deliberately stale `If-Match` header was accepted with `200` and applied anyway, overwriting the newer value. No `409` response is defined on any of the 53 paths, no version field is documented as a write guard, and no concurrency limits or behavior are published. Two integrations writing the same card will silently clobber each other. [live probes 30–31, W05–W09, 2026-09-03; `openapi.yaml` — no `409` response, no `If-Match` parameter]
- **C2.10 Versioning and backward compatibility: no** — no version identifier in the request contract. The specification carries `info.version: "1.0"` as metadata, but every one of the 53 paths is unversioned (`/api/...`), and grepping the specification for `/v1`, `api-version`, `X-Api-Version`, and `Accept-Version` returns nothing. No backward-compatibility policy defining breaking versus non-breaking changes and no deprecation window or notice policy appears anywhere in the documentation corpus. Practice is better than policy — one endpoint is labeled "Deprecated alias" with the legacy path still live, and the changelog notes when a change preserves existing behavior — but there is no version contract to build against. (Currency of change communication is graded separately in C4.4 and is strong; that evidence is not reused here.) [`openapi.yaml` paths and `info`; `llms-full.txt` — no backward-compatibility or deprecation-window policy; `api-reference/board/add-a-tab-view-legacy.md`]
- **C2.11 Request traceability: partial** — an identifier is present on every response but is not Aptly's and is not documented. Every live response carried a unique `CF-Ray` (for example `a356f85efc622a56-SJC`), which is Cloudflare's edge identifier. Aptly publishes no request or correlation identifier, and its documentation corpus never mentions `CF-Ray`, a request ID, or any trace mechanism usable with support. [live — 99 individually captured calls, all carrying `CF-Ray`, none carrying an Aptly-issued identifier, 2026-09-03; `llms-full.txt` — no request-identifier or support-trace guidance]
- **C2.12 Service availability and status transparency: no** — no public availability signal. `status.getaptly.com` resolves through Cloudflare but serves an unterminated 301 redirect loop; `/`, `/history`, and `/api/v2/status.json` are all unretrievable, and there is no status page at `docs.getaptly.com/status` or `www.getaptly.com/status` (both 404). No uptime figure, incident history, or SLA language appears anywhere in the documentation corpus. [live probes, 2026-09-03; `llms-full.txt` — zero occurrences of "status page", "uptime", "SLA", or "incident"]

Score math: earned 4.5 of 11 applicable checks (yes: C2.5, C2.7 = 2.0; partial: C2.1, C2.2, C2.3, C2.6, C2.11 = 2.5; no: C2.4, C2.9, C2.10, C2.12 = 0). C2.8 is unverified and excluded from the score. Unrounded fraction = 4.5 ÷ 11 = 0.40909; category points = 0.40909 × 10 = **4.1/10**; verification coverage = 11 verified ÷ 12 applicable = **92%**

**What this means for you:** This is the weakest part of Aptly's API, and it is weak in a specific way — the everyday experience is good, but the guarantees you would want before trusting it with unattended automation are missing. What works well: errors come back clean and machine-readable, paging through cards is solid and predictable, and pulling a full dataset or just what changed is genuinely easy. What is missing matters. If a write times out and your code retries, you can get a duplicate card, because there is no way to say "this is the same request." If two of your automations touch the same card at once, the second silently wins and the first change is lost — I confirmed that by sending a deliberately out-of-date update and watching Aptly accept it. There is no version number on the API and no published policy about breaking changes, so if Aptly changes something your integration depends on, your only warning is the changelog. And there is no working status page, so when something breaks you cannot tell whether it is you or them. One practical quirk worth knowing: after you write to a card, reading it back immediately can still show the old value — my archive looked like it had failed for two minutes before it appeared. Build in a delay before you verify a write, or you will chase problems that are not there.

## Category 3: Access Control and Safe Automation: 5.0/5
- **C3.1 Read-only credentials: yes** — keys can be restricted to a subset of read, insert, and update permissions: "Board API keys (`x-token`) can now be scoped to specific boards and to a subset of read/insert/update permissions, configurable from Setup → Developer → Board API Tokens. Requests outside a key's allowed boards or permissions now receive a `403 FORBIDDEN`." A read-only key is a key granted `read` alone. [`changelog.md`, 2026-08-13; corroborated in `openapi.yaml` `info.description`]
- **C3.2 Scoped credentials: yes** — fine-grained on both axes named by the check: by resource (specific `boardIds`) and by action (`read` / `insert` / `update`), enforced with `403 FORBIDDEN`. Independently corroborated live: the operator's key returned only the four boards with API access enabled, not the company's full board set. `POST /api/web-forms/{formId}` documents an even narrower form-specific key — "a board-scoped insert key can only submit to the one form it was created for." [`changelog.md` 2026-08-13 and 2026-08-16; `openapi.yaml` `info.description`; live probe 03, 2026-09-03]
- **C3.3 Multiple keys: yes** — keys are created individually with a name and an optional expiration ("Click **Create New Key**, enter a name, and optionally set an expiration date"), managed as a list under Setup → Developer → Board API Tokens, and the documentation's own best practice assumes several coexist: "Use a single API key per integration." [`authentication.md`, "API keys"; `rate-limits.md`, "Best practices"; `changelog.md` 2026-08-13]
- **C3.4 Rotation and revocation: yes** — self-serve, no support ticket. Keys are created in-product with an optional expiration date, and revocation is archiving the key from the same Setup → Developer → Board API Tokens screen: "Keys without an expiration remain active until archived. Expired or archived keys return `401`." The whole API can also be switched off per board from Card Sources → API. [`authentication.md`, "API keys" and "Key expiration"; `changelog.md` 2026-08-13]
- **C3.5 Test and production isolation: N-A** — no sandbox or separate test environment exists. `openapi.yaml` declares exactly one server (`https://core-api.getaptly.com`, "Production"), and the documentation corpus contains zero occurrences of "sandbox", "test environment", or "staging". With no test environment, the check does not apply; the absence is what forced this run's write testing onto the controlled live-data protocol. [`openapi.yaml` `servers`; `llms-full.txt`]

Score math: earned 4.0 of 4 applicable checks (C3.5 N-A, excluded); unrounded fraction = 1.0000; category points = 1.0000 × 5 = **5.0/5**; verification coverage = 4/4 = **100%**

**What this means for you:** Full marks, and this is the part that matters most for handing Aptly access to an AI agent. You can issue a key that can only read, only touch the boards you name, and nothing else — and you can kill it yourself in seconds without emailing anyone. That is exactly the control you want before letting an automation loose on tenant data. The one gap is that Aptly has no practice environment: there is only your live account. That is why I had to ask your permission before creating a test card today, and it is why any future build should be tested with a read-only key first, then given write access only once you have seen it behave.

## Category 4: Documentation and AI-Agent Readiness: 5.0/5
- **C4.1 Complete self-serve reference: yes** — complete, public, and example-rich, with no login and no reverse-engineering required. An 89-entry indexed reference covers authentication (`x-token` header, with an explicit warning against query-string credentials), pagination, rate limits, field-type value formats, and delegate tokens, plus a dedicated page per endpoint. Core endpoints carry worked request and response examples: the card-create page publishes the request schema with an example body (`{name: "John Smith", abc123: "john@example.com", ghi789: 1500}`) and the full response schema. Live cross-check: the specification's documented parameters, error statuses, and response envelope for the card endpoints matched observed behavior. Noted but not penalized here: the separate customer Help Center (last updated 2026-05-26) contradicts the developer portal in three places — it says the API "only supports creating cards on a board" against 53 documented paths, tells readers to pass the token "via an `x-token` query parameter" where the portal warns against exactly that, and labels the API endpoint list "Webhooks." The check grades the reference a developer builds from, and that reference is complete and correct. [`llms.txt`; `authentication.md`; `api-reference/cards/create-or-update-a-card.md`; Help Center page captured 2026-09-03]
- **C4.2 Reliable machine-consumable integration path: yes** — two complete, maintained mechanisms, either of which suffices. First, a genuine OpenAPI 3.0.3 specification (157,565 bytes, 53 paths, 24 named component schemas, three declared security schemes) suitable for code and tool generation, served both from the docs domain and unauthenticated from `GET /api/docs/openapi`. Second, a first-party operations-capable MCP server, documented tool by tool with scopes and throttles, exposing write operations including `create-card`, `update-card-field`, `create-card-comment`, `create-card-task`, `send-email`, and `send-sms` — verified reachable live in this session against the operator's account. One caveat that costs nothing here because the specification is complete: `https://docs.getaptly.com/api-reference/openapi.json`, linked from `llms.txt` as an OpenAPI spec, actually serves the Mintlify sample "OpenAPI Plant Store" document and is not an Aptly artifact. Note also that the "Aptly SDK" is a browser embed SDK (`window.aptly`) for iframe apps, not a server-side API client, so it is not the qualifying mechanism. [`openapi.yaml`; `mcp-server.md`; `aptly-sdk-reference.md`; live MCP tool invocation, 2026-09-03]
- **C4.3 AI-readable documentation: yes** — comprehensive and purpose-built for retrieval. `llms.txt` indexes all 89 pages with one-line descriptions; `llms-full.txt` is a 250,953-byte complete corpus; every documentation page and every endpoint page is separately retrievable as Markdown at a predictable `.md` URL; and a dedicated `llm-context.md` exists specifically to be pasted into an AI assistant. Each page even carries a header pointing at the index. This run's entire documentation packet was assembled from these files. [`llms.txt`; `llms-full.txt`; `llm-context.md`; per-endpoint `.md` URLs]
- **C4.4 Kept current: yes** — a dated changelog with clear, specific, per-endpoint entries, current to 2026-08-24, ten days before this run, with 23 dated entries running back to 2026-04-07. Entries name the exact endpoint and behavior changed, distinguish "New Endpoint" from "Enhancement" from "Fix", and state compatibility effects where they exist ("Existing keys are unaffected — an absent `boardIds`/`permissions` on a key means full access to all boards, as before"). The MCP tool reference is auto-generated from source, which keeps it in step by construction. This grades currency of change communication only; the versioning contract is graded in C2.10, where it fails, and that evidence is not reused here. [`changelog.md`; `mcp-server.md` header: "Auto-generated from `src/mcp/**/*.tools.ts`"]

Score math: earned 4.0 of 4 applicable checks; unrounded fraction = 1.0000; category points = 1.0000 × 5 = **5.0/5**; verification coverage = 4/4 = **100%**

**What this means for you:** This is the best thing about Aptly's API and it is not a close call. The documentation is complete, public, honest, and specifically built so that an AI assistant can read it and write correct code — there is a single file you can hand Claude that contains the entire API. That is why your renewal sync was straightforward to build and why the next one will be too. Aptly also runs its own MCP server, which is the shortest path for anything conversational. Two small things to know: one of the links Aptly advertises as an OpenAPI file actually serves an unrelated sample document, so ignore it and use `openapi.yaml`; and Aptly's customer Help Center is months out of date and contradicts the developer docs in a few places — trust `docs.getaptly.com`, not the Help Center.

## Category 5: Accessibility and Cost: 7.5/15
- **C5.1 Self-serve API key: yes** — once an account is entitled, credential creation is entirely self-serve with no sales call, support ticket, or approval step: "Open the board in Aptly → Go to **Card Sources → API** → Toggle the API **on** → Click **Create New Key**, enter a name, and optionally set an expiration date → Copy the key." The Help Center describes the same in-product path via the board's Integrations menu. Corroborated live: the operator's own self-created key authenticated and worked across 99 individually captured calls. Plan eligibility is scored separately in C5.3 and is not counted against this check. [`authentication.md`, "API keys"; Help Center, "How to Enable the API"; live probe 01, 2026-09-03]
- **C5.3 Not commercially gated: no** — API access requires a premium plan. Aptly's own Help Center states of the API: "It does require a Premium Subscription. If you're interested in upgrading, please reach out to sales@getaptly.com or simply upgrade in your account. Go to Settings > Subscription > Manage > Upgrade under Premium." That is explicit top-tier gating, and it is not identity or regulatory verification, so the check's KYC/KYB carve-out does not apply. Corroborated by the pricing page: https://www.getaptly.com/pricing publishes three named tiers — Essential, Premium and Enterprise — with prices, and lists "Access to Aptly API" as a Premium plan feature and "Enterprise API" under Enterprise. That independently confirms the Help Center's Premium requirement, so an operator can both see the gate and price it [rechecked live 2026-09-09]. Mitigating in a small way: the upgrade itself can be self-served in-account rather than requiring the sales email. Aptly's developer portal never mentions the premium requirement, which is why this check moved from a provisional `yes` to `no` during the controlled verification pass. [Help Center page captured 2026-09-03, last updated 2026-05-26; https://www.getaptly.com/pricing]

Score math: earned 1.0 of 2 applicable checks (yes: C5.1 = 1.0; no: C5.3 = 0); unrounded fraction = 0.5000; category points = 0.5000 × 15 = **7.5/15**; verification coverage = 2/2 = **100%**

**What this means for you:** You are already through the door — your key works, and you created it yourself without asking Aptly's permission. But this category is scored for any operator, and for most of them the API sits behind Aptly's Premium plan, with no published price to evaluate. That costs Aptly half of the single heaviest category in the rubric. It is worth knowing for a practical reason too: if your subscription is ever downgraded, the API and every automation you have built on it stop working, and nothing in the developer documentation warns you of that dependency.

## Total
- Raw: **29.09 / 50** (7.5000 + 4.0909 + 5.0000 + 5.0000 + 7.5000)
- Normalized before rounding: **58.18 / 100**
- Published numeric score: **58 / 100**
- Letter grade: **F**
- Evidence tier: Fully verified — controlled live
- Overall verification coverage: **96%** (25 verified of 26 applicable checks; gate: no category Unable to verify — lowest is Category 2 at 92%; overall ≥ 80% ✓; minimum battery complete ✓)
- Partial-result flag: **yes, immaterial.** C2.8 is unverified. Both resolutions were computed: if webhooks do not exist, C2.8 is N-A and Category 2 scores 4.09/10 for a raw total of 29.09 and a published **58**; if they exist but are undocumented and unsigned, C2.8 is `no`, Category 2 scores 3.75/10, the raw total is 28.75, normalized 57.50, which rounds up to a published **58**. The score and the letter grade are identical either way. What would resolve it: a first-party webhook reference from Aptly, or an operator observation of a webhook or outbound-URL configuration in a board's Integrations → API panel.
- Unresolved evaluator disagreements: **none carried forward**, but two checks were close calls and are recorded so a reproducing evaluator can see the reasoning. **C2.3** was weighed between `partial` and `no`: the `no` band names "success codes that hide failures", and the silently ignored `updatedAtMin` value is exactly that, but the reported-error contract satisfies every element of the `yes` band across ten probes, so `partial` was chosen as the honest middle. Marking it `no` would move Category 2 to 3.64/10 and the published score to 57 (grade F, unchanged). **C2.2** was weighed between `partial` and `no`: the incorrect core `Error` schema and the three-way envelope inconsistency argue for "types vary across endpoints", but value typing was flawless across 190 live records, so `partial` was chosen. Marking it `no` would move Category 2 to 3.64/10 and the published score to 57 (grade F, unchanged). Neither close call changes the grade.

## Bottom line for a property manager
Aptly's API is the best-documented API in your stack and one of the least guaranteed — a genuinely useful tool with almost none of the safety rails you would want around unattended automation. Today you can build exactly what you are already building: read every board, card, contact, and task; create and update cards; fire your board workflows by changing a card's stage; and pull either a full dataset or just what changed since last night. I verified all of that against your live account this afternoon, including creating, updating, and archiving a test card on your Leases board. The documentation is outstanding — a complete OpenAPI file, a whole-API text corpus written for AI assistants, a per-endpoint reference, a changelog current to ten days ago, and Aptly's own MCP server — and its access controls are excellent, letting you hand an AI agent a key that can only read, only on the boards you choose, revocable by you in seconds.

What you cannot build is anything that needs to react the moment something happens, or anything that must not be allowed to go wrong quietly. Aptly has no webhooks, so every integration polls on a timer. It has no way to mark a write as a retry, so a timeout followed by a retry can create a duplicate card. It has no protection against two automations overwriting each other — I proved that by sending a deliberately stale update and watching Aptly accept it. There is no version number on the API and no published policy on breaking changes, no working status page to check when something fails, and no practice environment, so every test happens in your live account. The score is 58 out of 100, an F, and it lands there for two concrete reasons: the write and reliability guarantees above, and the fact that Aptly gates the API behind an unpublished Premium plan.

None of that makes Aptly the wrong tool — it is a property-management-specialized workflow and communication layer, and that is the job it does. But it is not a system of record and not a bank: it holds no funds, has no ledger, and documents no trust, escrow, or security-deposit accounting. Rentvine remains your source of truth for leases, money, and work orders, and Aptly's API is best used the way you use it now — to read and write workflow state on a schedule, with a read-only key wherever a read-only key will do, and with a deliberate pause before verifying any write, because Aptly will sometimes show you the old value for a minute or two after a change has actually been applied.
