{
  "info": {
    "_postman_id": "319ade8f-14ae-4ff2-94d5-bdf8d2154f6d",
    "name": "Lit Circle Bookstore Events API",
    "description": "Test Lit Circle's bookstore-scoped Events API with a dedicated, revocable credential. Import the companion environment template, add the credential only to your local environment, and keep allowWrites set to false until you intentionally want to create, replace, or delete production Event data. Never export or share an environment after adding a credential.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{apiKey}}",
        "type": "string"
      }
    ]
  },
  "event": [
    {
      "listen": "prerequest",
      "script": {
        "type": "text/javascript",
        "exec": [
          "if (!pm.environment.get('apiKey')) {",
          "  throw new Error('Set apiKey in the active Lit Circle environment before sending requests.');",
          "}"
        ]
      }
    }
  ],
  "item": [
    {
      "name": "Taxonomy",
      "description": "Read the current labels accepted by Event Source writes before constructing a payload.",
      "item": [
        {
          "name": "List accepted Event Types",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/event-types",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "event-types"]
            },
            "description": "Returns the Event Type labels currently accepted by POST and PUT."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Returns accepted Event Types', function () {",
                  "  pm.response.to.have.status(200);",
                  "  pm.expect(pm.response.json().data).to.be.an('array');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "List accepted Genres",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/genres",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "genres"]
            },
            "description": "Returns the Genre labels currently accepted by POST and PUT."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Returns accepted Genres', function () {",
                  "  pm.response.to.have.status(200);",
                  "  pm.expect(pm.response.json().data).to.be.an('array');",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "List available Neighborhoods",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/neighborhoods",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "neighborhoods"]
            },
            "description": "Returns Neighborhood IDs and names available in the credential's bookstore Area."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Returns available Neighborhoods', function () {",
                  "  pm.response.to.have.status(200);",
                  "  pm.expect(pm.response.json().data).to.be.an('array');",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Read Events",
      "description": "Read the complete bookstore-associated Event snapshot or one associated Public Event.",
      "item": [
        {
          "name": "List bookstore Events",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/events",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "events"]
            },
            "description": "Returns the authoritative default Event Feed Snapshot. Save its ETag for conditional polling."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Returns an Event Feed Snapshot', function () {",
                  "  pm.response.to.have.status(200);",
                  "  pm.expect(pm.response.json().data).to.be.an('array');",
                  "});",
                  "const etag = pm.response.headers.get('ETag');",
                  "if (etag) pm.collectionVariables.set('etag', etag);"
                ]
              }
            }
          ]
        },
        {
          "name": "Poll bookstore Events conditionally",
          "request": {
            "method": "GET",
            "header": [
              {
                "key": "If-None-Match",
                "value": "{{etag}}",
                "type": "text"
              }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/events",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "events"]
            },
            "description": "Reuses the ETag captured by List bookstore Events. An unchanged snapshot returns 304."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Returns a snapshot or Not Modified', function () {",
                  "  pm.expect(pm.response.code).to.be.oneOf([200, 304]);",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Get one associated Event",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/events/{{eventId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "events", "{{eventId}}"]
            },
            "description": "Returns one associated Public Event. Set eventId manually or run the create request first."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Returns the requested Event', function () {",
                  "  pm.response.to.have.status(200);",
                  "  pm.expect(pm.response.json().data.id).to.eql(pm.collectionVariables.get('eventId'));",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Write Events",
      "description": "These requests write production Event data. They are blocked until allowWrites is explicitly set to true in collection variables.",
      "event": [
        {
          "listen": "prerequest",
          "script": {
            "type": "text/javascript",
            "exec": [
              "if (pm.collectionVariables.get('allowWrites') !== 'true') {",
              "  throw new Error('Production writes are disabled. Set collection variable allowWrites to true only when you intend to modify Event data.');",
              "}"
            ]
          }
        }
      ],
      "item": [
        {
          "name": "Create a source-managed Event",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"externalEventId\": \"{{externalEventId}}\",\n  \"sourceUpdatedAt\": \"{{sourceUpdatedAt}}\",\n  \"sourceState\": \"scheduled\",\n  \"sourceUrl\": \"https://bookstore.example/events/{{externalEventId}}\",\n  \"title\": \"Postman API Test Event\",\n  \"description\": {\n    \"markdown\": \"Created intentionally from the Lit Circle Postman collection.\"\n  },\n  \"schedule\": {\n    \"startsAt\": \"{{startsAt}}\",\n    \"endsAt\": \"{{endsAt}}\"\n  },\n  \"eventTypes\": [\"Author Event\"],\n  \"genres\": [],\n  \"ageRating\": \"allAges\",\n  \"venue\": {\n    \"kind\": \"bookstore\"\n  },\n  \"admission\": {\n    \"isFree\": true,\n    \"priceText\": null,\n    \"ticketIncludesBook\": false,\n    \"registration\": {\n      \"required\": false,\n      \"url\": null\n    },\n    \"ticketUrl\": null\n  },\n  \"image\": null\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/events",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "events"]
            },
            "description": "Creates one source-managed Event. The pre-request script generates a stable External Event ID and timestamp for safe identical retries. Clear externalEventId and sourceUpdatedAt before intentionally creating another Event.\n\nOptional catalog fields can be added to the normal body (replace placeholder IDs with real saved IDs):\n\n```json\n{\n  \"people\": [{\"id\": \"saved-person-id\", \"role\": \"author\"}, {\"name\": \"Example Presenter\", \"role\": \"presenter\"}],\n  \"books\": [{\"isbn\": \"9780140328721\", \"role\": \"featured\"}]\n}\n```\n\nSaved records resolve first; missing books are looked up by ISBN or exact title. People use saved ID or a unique name; books use saved ID, ISBN, or a unique exact title. Unknown IDs and ambiguous matches return field-specific 422 errors. Each collection allows up to eight entries; only one book may be featured. Omitted people/books preserve existing enrichment, arrays replace their collection, and [] clears it. Shared profiles and confirmed book authorship remain unchanged; book credits do not imply event participation. Book lookup failures return 502/503 unavailable without saving changes. Identical retries skip lookup."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('externalEventId')) {",
                  "  pm.collectionVariables.set('externalEventId', 'postman-' + Date.now());",
                  "}",
                  "if (!pm.collectionVariables.get('sourceUpdatedAt')) {",
                  "  pm.collectionVariables.set('sourceUpdatedAt', new Date().toISOString());",
                  "}",
                  "if (!pm.collectionVariables.get('startsAt')) {",
                  "  const startsAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);",
                  "  pm.collectionVariables.set('startsAt', startsAt.toISOString());",
                  "  pm.collectionVariables.set('endsAt', new Date(startsAt.getTime() + 90 * 60 * 1000).toISOString());",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Creates or idempotently replays the Event', function () {",
                  "  pm.expect(pm.response.code).to.be.oneOf([200, 201]);",
                  "});",
                  "const payload = pm.response.json();",
                  "if (payload.data && payload.data.id) pm.collectionVariables.set('eventId', payload.data.id);"
                ]
              }
            }
          ]
        },
        {
          "name": "Replace a source-managed Event",
          "request": {
            "method": "PUT",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json",
                "type": "text"
              }
            ],
            "body": {
              "mode": "raw",
              "raw": "{\n  \"externalEventId\": \"{{externalEventId}}\",\n  \"sourceUpdatedAt\": \"{{replacementSourceUpdatedAt}}\",\n  \"sourceState\": \"scheduled\",\n  \"sourceUrl\": \"https://bookstore.example/events/{{externalEventId}}\",\n  \"title\": \"Postman API Test Event — Updated\",\n  \"description\": {\n    \"markdown\": \"Replaced intentionally from the Lit Circle Postman collection.\"\n  },\n  \"schedule\": {\n    \"startsAt\": \"{{startsAt}}\",\n    \"endsAt\": \"{{endsAt}}\"\n  },\n  \"eventTypes\": [\"Author Event\"],\n  \"genres\": [],\n  \"ageRating\": \"allAges\",\n  \"venue\": {\n    \"kind\": \"bookstore\"\n  },\n  \"admission\": {\n    \"isFree\": true,\n    \"priceText\": null,\n    \"ticketIncludesBook\": false,\n    \"registration\": {\n      \"required\": false,\n      \"url\": null\n    },\n    \"ticketUrl\": null\n  },\n  \"image\": null\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            },
            "url": {
              "raw": "{{baseUrl}}/api/v1/events/{{eventId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "events", "{{eventId}}"]
            },
            "description": "Replaces the source-managed field set for the Event captured by the create request. Clear replacementSourceUpdatedAt before preparing a newer replacement.\n\nOptional catalog fields can be added to the normal body (replace placeholder IDs with real saved IDs):\n\n```json\n{\n  \"people\": [{\"id\": \"saved-person-id\", \"role\": \"author\"}, {\"name\": \"Example Presenter\", \"role\": \"presenter\"}],\n  \"books\": [{\"isbn\": \"9780140328721\", \"role\": \"featured\"}]\n}\n```\n\nSaved records resolve first; missing books are looked up by ISBN or exact title. People use saved ID or a unique name; books use saved ID, ISBN, or a unique exact title. Unknown IDs and ambiguous matches return field-specific 422 errors. Each collection allows up to eight entries; only one book may be featured. Omitted people/books preserve existing enrichment, arrays replace their collection, and [] clears it. Shared profiles and confirmed book authorship remain unchanged; book credits do not imply event participation. Book lookup failures return 502/503 unavailable without saving changes. Identical retries skip lookup."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('eventId') || !pm.collectionVariables.get('externalEventId')) {",
                  "  throw new Error('Run Create a source-managed Event first, or set eventId and externalEventId manually.');",
                  "}",
                  "if (!pm.collectionVariables.get('replacementSourceUpdatedAt')) {",
                  "  pm.collectionVariables.set('replacementSourceUpdatedAt', new Date().toISOString());",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Replaces the source-managed Event', function () {",
                  "  pm.response.to.have.status(200);",
                  "  pm.expect(pm.response.json().data.id).to.eql(pm.collectionVariables.get('eventId'));",
                  "});"
                ]
              }
            }
          ]
        },
        {
          "name": "Delete an API-created Event",
          "request": {
            "method": "DELETE",
            "header": [],
            "url": {
              "raw": "{{baseUrl}}/api/v1/events/{{eventId}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "events", "{{eventId}}"]
            },
            "description": "Permanently deletes the Event captured by the create request. Only API-created Events belonging to this credential's bookstore are eligible."
          },
          "event": [
            {
              "listen": "prerequest",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "if (!pm.collectionVariables.get('eventId')) {",
                  "  throw new Error('Run Create a source-managed Event first, or set eventId manually.');",
                  "}"
                ]
              }
            },
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('Deletes the API-created Event', function () {",
                  "  pm.response.to.have.status(204);",
                  "});"
                ]
              }
            }
          ]
        }
      ]
    }
  ],
  "variable": [
    {
      "key": "baseUrl",
      "value": "https://litcircle.org",
      "type": "string"
    },
    {
      "key": "allowWrites",
      "value": "false",
      "type": "string"
    },
    {
      "key": "eventId",
      "value": "",
      "type": "string"
    },
    {
      "key": "externalEventId",
      "value": "",
      "type": "string"
    },
    {
      "key": "sourceUpdatedAt",
      "value": "",
      "type": "string"
    },
    {
      "key": "replacementSourceUpdatedAt",
      "value": "",
      "type": "string"
    },
    {
      "key": "startsAt",
      "value": "",
      "type": "string"
    },
    {
      "key": "endsAt",
      "value": "",
      "type": "string"
    },
    {
      "key": "etag",
      "value": "",
      "type": "string"
    }
  ]
}
