{
  "openapi": "3.1.0",
  "info": {
    "title": "ParentLove Public Feature Manifest",
    "version": "2.2.1",
    "summary": "Read-only description of the public ParentLove feature manifest.",
    "description": "Describes the single public JSON document that ParentLove publishes about the ParentLove: Baby Tracker app: identity, current release per platform, durable capabilities with their free/paid boundary, US pricing, privacy facts, store facts and official links. The canonical document is generated by the site from the same facts as its JSON-LD and served at /wp-json/parentlove/v1/manifest; the legacy static path /wp-content/uploads/features.json answers with a 301 to that endpoint since contract 2.2.1 (v1.26). Every product page references the manifest as <link rel=\"describedby\" type=\"application/json\"> and as MobileApplication.supportingData in the page JSON-LD, and this description as <link rel=\"service-desc\" type=\"application/vnd.oai.openapi+json\" href=\"/openapi.json\">. Shape 2.2 (built 2026-10-31; the first bump after publication) removes currentRelease.version (the platform objects are the authoritative versions), moves the release history into app.history (first released in 2012 on Windows Phone, on iOS and Android since 2015, the Google Play release date) and makes links.openapi the JSON contract with links.openapiYaml as the optional YAML twin (links.openapiJson dropped). Shape 2.1 (2026-10-30, published 2026-10-31) added the awards list, app.designedBy, publisher.alternateName and the optional links.reddit; shape 2.0 was never published. Every object is closed (additionalProperties false) and schema_version is a const, so a reader that validates against an older document must be updated at every bump; a reader of known fields is affected by 2.2 only where it read currentRelease.version, storeFacts.firstReleased, storeFacts.googlePlayReleasedOn or links.openapiJson. Dates: every date in the manifest is an ISO 8601 calendar date (YYYY-MM-DD) or year (YYYY) without a time or a zone; dateModified is the UTC calendar date on which the published content last materially changed, and the endpoint's Last-Modified header is that date at 00:00:00 UTC. Contract 2.2.1 (v1.26, 2026-10-31) changes delivery only, the manifest shape stays 2.2: the YAML twin at /openapi.yaml is served as application/yaml and this JSON document as application/json, both with Cache-Control public, max-age=3600, must-revalidate, a weak ETag and Last-Modified (a conditional request answers 304), and the legacy static path redirects. Prefer the JSON twin, which the pages link as service-desc. Until v1.25 the host served the YAML twin as application/octet-stream and the static path as a one-year-cached manual copy.\n",
    "contact": {
      "name": "ParentLove Customer Care",
      "email": "care@parentlove.me",
      "url": "https://parentlove.me/"
    }
  },
  "servers": [
    {
      "url": "https://parentlove.me"
    }
  ],
  "paths": {
    "/wp-json/parentlove/v1/manifest": {
      "get": {
        "summary": "Returns the current ParentLove feature manifest (canonical, generated from the site's single facts source)",
        "operationId": "getFeatureManifest",
        "tags": [
          "manifest"
        ],
        "responses": {
          "200": {
            "description": "Current ParentLove feature manifest. Response headers as served to anonymous requests (a logged-in WordPress user receives WordPress's no-cache headers instead): Content-Type application/json; charset=UTF-8; Cache-Control public, max-age=3600; Last-Modified = dateModified at 00:00:00 UTC (an HTTP-date); ETag = a weak validator over the response body (W/\"<32 hex>\", it changes with any content edit); X-Robots-Tag noindex (the WordPress REST default: the document describes the app and is not meant to be indexed as a page). Floats (priceUSD) are serialised with PHP's shortest round-trip precision (24.99 rather than 24.989999999999998) since v1.24 (contract 2.2.0), which pins that precision for this route's request.\n",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeatureManifest"
                }
              }
            }
          }
        }
      }
    },
    "/wp-content/uploads/features.json": {
      "get": {
        "deprecated": true,
        "summary": "Legacy static path of the manifest (a manual copy until v1.25). Since contract 2.2.1 (v1.26) WordPress answers it with a 301 to the canonical endpoint once the static file is deleted; follow the redirect.",
        "operationId": "getFeatureManifestStaticCopy",
        "tags": [
          "manifest"
        ],
        "responses": {
          "301": {
            "description": "Permanent redirect to /wp-json/parentlove/v1/manifest (Location header; Cache-Control public, max-age=3600; no body). Until v1.25 this path served a one-year-cached static copy of the manifest with HTTP 200 (Content-Type application/json, Cache-Control public, max-age=31536000); a client that cached that copy sees the redirect once its cached entry expires or the edge cache is purged.",
            "headers": {
              "Location": {
                "description": "The canonical manifest URL.",
                "schema": {
                  "type": "string",
                  "format": "uri"
                }
              },
              "Cache-Control": {
                "description": "public, max-age=3600 (the redirect itself may be cached for an hour).",
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "FeatureManifest": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "schema_version",
          "dateModified",
          "app",
          "capabilities",
          "awards",
          "pricing",
          "privacy",
          "links"
        ],
        "properties": {
          "schema_version": {
            "type": "string",
            "const": "2.2",
            "description": "Version of the manifest JSON shape (not the app version). 2.2 removed currentRelease.version, replaced storeFacts.firstReleased and storeFacts.googlePlayReleasedOn with app.history and replaced links.openapiJson with links.openapiYaml (links.openapi now points at the JSON contract); 2.1 added awards, app.designedBy, publisher.alternateName and the optional links.reddit.\n"
          },
          "dateModified": {
            "$ref": "#/components/schemas/IsoDate",
            "description": "UTC calendar date on which the published manifest content last materially changed: the newest of the generator's explicit content-change date (set to the day a change goes live), the two platform release dates, the Google Play \"Updated on\" date and the privacy policy date. The endpoint's Last-Modified header is this date at 00:00:00 UTC.\n"
          },
          "app": {
            "$ref": "#/components/schemas/App"
          },
          "capabilities": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Capability"
            }
          },
          "awards": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Award"
            },
            "description": "Official award listings, newest year first (an empty list is legal: the JSON-LD then carries no award key). The same list feeds the page JSON-LD: titles with scope \"app\" sit on the MobileApplication node, titles with scope \"company\" on the Organization node.\n"
          },
          "pricing": {
            "$ref": "#/components/schemas/Pricing"
          },
          "privacy": {
            "$ref": "#/components/schemas/Privacy"
          },
          "links": {
            "$ref": "#/components/schemas/Links"
          }
        }
      },
      "App": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "shortName",
          "publisher",
          "identifiers",
          "releaseFamily",
          "currentRelease",
          "history",
          "storeFacts"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 3,
            "maxLength": 80,
            "description": "Public store title (Apple App Store and Google Play), e.g. \"ParentLove: Baby Tracker\"."
          },
          "shortName": {
            "type": "string",
            "minLength": 2,
            "maxLength": 40
          },
          "publisher": {
            "$ref": "#/components/schemas/Publisher"
          },
          "designedBy": {
            "type": "string",
            "minLength": 3,
            "maxLength": 300,
            "description": "A selection of the designer's credentials relevant to a baby tracker (the About page's credentials list is the complete source), plus \"mother of two\" as both store listings state it (\"Mom & IBCLC Designed\"; \"Designed by an IBCLC & Baby Care Specialist\"). A prose string by design; a structured form would be a later, separate property.\n"
          },
          "identifiers": {
            "$ref": "#/components/schemas/Identifiers"
          },
          "releaseFamily": {
            "type": "string",
            "description": "Marketing name of the current major release family, e.g. \"ParentLove 12\".",
            "pattern": "^ParentLove [0-9]+$"
          },
          "currentRelease": {
            "$ref": "#/components/schemas/CurrentRelease"
          },
          "history": {
            "$ref": "#/components/schemas/History"
          },
          "storeFacts": {
            "$ref": "#/components/schemas/StoreFacts"
          }
        }
      },
      "Publisher": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "name",
          "legalName"
        ],
        "properties": {
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 80
          },
          "legalName": {
            "type": "string",
            "minLength": 2,
            "maxLength": 120
          },
          "alternateName": {
            "type": "string",
            "minLength": 2,
            "maxLength": 80,
            "description": "The developer name the stores show (Google Play \"by Coquisoft\")."
          }
        }
      },
      "Identifiers": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "appleAppId",
          "androidPackage"
        ],
        "properties": {
          "appleAppId": {
            "type": "string",
            "const": "1038554631",
            "description": "Apple App Store numeric app ID (also the \"id\" in the store URL). Immutable."
          },
          "androidPackage": {
            "type": "string",
            "const": "com.coquisoft.parentlove",
            "description": "Android application ID (Google Play package name). Immutable."
          }
        }
      },
      "CurrentRelease": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "platforms",
          "sharedNotes",
          "highlights"
        ],
        "description": "The per-platform objects are the authoritative versions. Shape 2.1 also carried a headline \"version\" (the newer of the two platform versions); 2.2 removed it because a generic reader took it for the version on every platform while the platforms were on different versions.\n",
        "properties": {
          "platforms": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "ios",
              "android"
            ],
            "properties": {
              "ios": {
                "$ref": "#/components/schemas/PlatformRelease"
              },
              "android": {
                "$ref": "#/components/schemas/PlatformRelease"
              }
            }
          },
          "sharedNotes": {
            "type": "array",
            "description": "What's New items that apply to both platforms (plain, no prefix).",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          "highlights": {
            "type": "array",
            "minItems": 1,
            "maxItems": 12,
            "description": "Derived list: each platform's notes prefixed \"iOS:\" / \"Android:\" (or \"iOS <version>:\" / \"Android <version>:\" while the platforms are on different versions), then the shared notes prefixed \"iOS and Android:\". The same items appear in MobileApplication.releaseNotes in the page JSON-LD, grouped per platform (\"iOS <version> (released <date>): item; item. Android <version> (released <date>): item. iOS and Android: item.\").\n",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 220
            }
          }
        }
      },
      "PlatformRelease": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "version",
          "releaseDate",
          "minimumOS",
          "notes"
        ],
        "properties": {
          "version": {
            "$ref": "#/components/schemas/AppVersion"
          },
          "releaseDate": {
            "$ref": "#/components/schemas/IsoDate",
            "description": "The store's own release date for the version (UTC): the App Store version date; on Google Play the \"Updated on\" date of the release (the staged-rollout start), which can precede the day the listing shows the version to everyone.\n"
          },
          "notes": {
            "type": "array",
            "minItems": 1,
            "description": "This platform's What's New items for its current version (plain text, no platform prefix).",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          "storeUpdatedOn": {
            "$ref": "#/components/schemas/IsoDate",
            "description": "Google Play only: the date shown as \"Updated on\" in the US listing. It moves with listing or rollout changes at the same version, so it can be later than releaseDate.\n"
          },
          "minimumOS": {
            "type": "string",
            "pattern": "^(iOS|Android) [0-9]+(\\.[0-9]+)* or later$"
          }
        }
      },
      "StoreFacts": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "languages",
          "ageRating",
          "storeCategories",
          "designedFor",
          "appleDevices"
        ],
        "description": "Durable facts exposed by the public store listings (captured 2026-10-21, re-checked 2026-10-26; versions, release dates and minimum OS re-checked 2026-10-31). The release history (first release year and platform, iOS and Android availability, the Google Play release date) lives in app.history since 2.2.",
        "properties": {
          "languages": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            }
          },
          "designedFor": {
            "type": "string",
            "description": "The device the App Store says the app is designed for (\"Designed for iPhone\")."
          },
          "ageRating": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "appleAppStore",
              "googlePlay"
            ],
            "properties": {
              "appleAppStore": {
                "type": "string"
              },
              "googlePlay": {
                "type": "string"
              }
            }
          },
          "storeCategories": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "appleAppStore",
              "googlePlay"
            ],
            "properties": {
              "appleAppStore": {
                "type": "string"
              },
              "googlePlay": {
                "type": "string"
              }
            }
          },
          "appleDevices": {
            "type": "array",
            "minItems": 1,
            "items": {
              "type": "string"
            },
            "description": "Entries of the App Store \"Compatibility\" panel plus iPad. The page label reads \"Only for iPhone\" and the panel has no iPad row; iPad compatibility comes from the listing's own JSON-LD (\"Compatible with iPhone, iPad, and iPod touch\") and the Lookup API supportedDevices. The app is designed for iPhone; the other devices run it in compatibility mode (the listing says \"Not verified for macOS\").\n"
          }
        }
      },
      "History": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "firstReleasedYear",
          "firstPlatform",
          "iosAndroidAvailableSinceYear",
          "googlePlayReleasedOn"
        ],
        "description": "Release history as the site states it (home FAQ \"When was ParentLove first released?\": first released in 2012 on Windows Phone, on iOS and Android since 2015) and as Google Play shows it (\"Released on Oct 31, 2015\"). Year-only facts carry a year-only field name; the App Store listing page shows no first-release date, so none is claimed. Added in 2.2, replacing storeFacts.firstReleased and storeFacts.googlePlayReleasedOn.\n",
        "properties": {
          "firstReleasedYear": {
            "type": "string",
            "pattern": "^[0-9]{4}$",
            "description": "Year of the first ParentLove release on any platform (2012, Windows Phone)."
          },
          "firstPlatform": {
            "type": "string",
            "minLength": 2,
            "maxLength": 40,
            "description": "The platform of that first release."
          },
          "iosAndroidAvailableSinceYear": {
            "type": "string",
            "pattern": "^[0-9]{4}$",
            "description": "Year the app became available on iOS and Android (the app the two store identifiers name; the page JSON-LD datePublished carries the same year)."
          },
          "googlePlayReleasedOn": {
            "$ref": "#/components/schemas/IsoDate",
            "description": "Release date as displayed on Google Play (US listing)."
          }
        }
      },
      "Capability": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "name",
          "tier"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z][A-Za-z0-9]*$",
            "description": "Stable machine identifier (lowerCamelCase)."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "tier": {
            "type": "string",
            "enum": [
              "free",
              "paid",
              "mixed"
            ],
            "description": "free = no purchase needed; paid = needs an upgrade; mixed = partly free."
          },
          "requires": {
            "type": "string",
            "maxLength": 200,
            "description": "Which one-time upgrade(s) or app version unlock the capability."
          }
        }
      },
      "Award": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "title",
          "year",
          "scope",
          "body"
        ],
        "properties": {
          "title": {
            "type": "string",
            "minLength": 3,
            "maxLength": 120,
            "description": "The award title exactly as the awarding body lists it."
          },
          "year": {
            "type": "string",
            "pattern": "^20[0-9]{2}$"
          },
          "scope": {
            "type": "string",
            "enum": [
              "app",
              "company"
            ],
            "description": "Whom the title names, the app or the company (decides which JSON-LD node carries it)."
          },
          "body": {
            "type": "string",
            "minLength": 3,
            "maxLength": 120,
            "description": "The awarding body, e.g. \"LUXlife Parent & Baby Awards\". Modelled per award; today every entry carries the same body, which the site's facts hold once.\n"
          }
        }
      },
      "Pricing": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "currency",
          "region",
          "note",
          "freeDownload",
          "subscription",
          "oneTimeUpgrades",
          "proTrialDays",
          "moneyBackGuaranteeDays"
        ],
        "properties": {
          "currency": {
            "type": "string",
            "pattern": "^[A-Z]{3}$"
          },
          "region": {
            "type": "string",
            "pattern": "^[A-Z]{2}$"
          },
          "note": {
            "type": "string",
            "description": "Free-text pricing note. It also states the optional free Pro trial and what happens to logged entries when the trial ends, in the wording of the Google Play listing, and the money-back guarantee in the wording of the site's home FAQ and Terms of Service (for upgrades; email support within the guarantee window for a full refund).\n"
          },
          "freeDownload": {
            "type": "boolean"
          },
          "subscription": {
            "type": "boolean"
          },
          "oneTimeUpgrades": {
            "type": "array",
            "minItems": 1,
            "items": {
              "$ref": "#/components/schemas/Upgrade"
            }
          },
          "proTrialDays": {
            "type": "integer",
            "minimum": 0
          },
          "moneyBackGuaranteeDays": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "Upgrade": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "id",
          "name",
          "priceUSD",
          "includes"
        ],
        "properties": {
          "id": {
            "type": "string",
            "pattern": "^[a-z][A-Za-z0-9]*$"
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "description": "Name as shown on parentlove.me/pricing/."
          },
          "storeName": {
            "type": "string",
            "minLength": 1,
            "description": "Apple App Store in-app-purchase label when it differs from the site name."
          },
          "priceUSD": {
            "type": "number",
            "minimum": 0,
            "description": "One-time US price in US dollars, as listed on /pricing/ and in the App Store IAP list."
          },
          "includes": {
            "type": "string"
          }
        }
      },
      "Privacy": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "dataSaleStatement",
          "encryptionInTransit",
          "encryptionAtRest",
          "encryptionAtRestScope",
          "adsStatement",
          "policyUrl",
          "policyLastUpdated"
        ],
        "properties": {
          "dataSaleStatement": {
            "type": "string",
            "description": "Verbatim sentence from the public Privacy Policy."
          },
          "encryptionInTransit": {
            "type": "string"
          },
          "encryptionAtRest": {
            "type": "string"
          },
          "encryptionAtRestScope": {
            "type": "string",
            "description": "What the at-rest encryption statement covers (server-side storage)."
          },
          "adsStatement": {
            "type": "string"
          },
          "policyUrl": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "policyLastUpdated": {
            "$ref": "#/components/schemas/IsoDate"
          }
        }
      },
      "Links": {
        "type": "object",
        "additionalProperties": false,
        "required": [
          "website",
          "appStore",
          "playStore",
          "features",
          "pricing",
          "privacyPolicy",
          "termsOfService",
          "manifest",
          "openapi",
          "support"
        ],
        "description": "Identity, store, contract and support links are required; social and developer-profile links are optional so a profile can be added or retired without a shape bump. The object is closed, so a reader that validates against the published document refreshes it first. Shape 2.2 repointed openapi at the JSON contract and replaced openapiJson with the optional openapiYaml; no other key was removed or renamed.\n",
        "properties": {
          "website": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "appStore": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "playStore": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "appleDeveloper": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "googlePlayDeveloper": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "youtube": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "facebook": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "instagram": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "tiktok": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "reddit": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "features": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "pricing": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "privacyPolicy": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "termsOfService": {
            "$ref": "#/components/schemas/HttpsUrl"
          },
          "manifest": {
            "$ref": "#/components/schemas/HttpsUrl",
            "description": "Canonical URL of this document (the REST endpoint)."
          },
          "openapi": {
            "$ref": "#/components/schemas/HttpsUrl",
            "description": "The OpenAPI 3.1 contract for this document in its JSON form (/openapi.json, the target of the pages' service-desc link). Until shape 2.1 this key pointed at the YAML form and openapiJson at the JSON form."
          },
          "openapiYaml": {
            "$ref": "#/components/schemas/HttpsUrl",
            "description": "Optional YAML twin of the contract (/openapi.yaml; served as application/yaml with a one-hour revalidation policy since contract 2.2.1)."
          },
          "support": {
            "type": "string",
            "format": "uri",
            "pattern": "^mailto:[^@\\s]+@[^@\\s]+$"
          }
        }
      },
      "AppVersion": {
        "type": "string",
        "pattern": "^[0-9]+\\.[0-9]+(\\.[0-9]+)?$",
        "description": "Two- or three-part app version as shown by the stores (e.g. 12.3.0, 12.4)."
      },
      "IsoDate": {
        "type": "string",
        "format": "date",
        "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
      },
      "HttpsUrl": {
        "type": "string",
        "format": "uri",
        "pattern": "^https://"
      }
    }
  }
}