{
  "info": {
    "name": "Nimbio Public API",
    "description": "Programmatic access to your Nimbio keys, latches, members, and opens.\n\n**Authentication** — every endpoint (except `/healthz`) takes a bearer token:\n`Authorization: Bearer nimbio_test_…` or `nimbio_live_…`. Click **Authorize**\nand paste a key to try calls here.\n\n**Test vs live** — a `nimbio_test_*` key runs the full pipeline (auth, rate\nlimit, scope, validation) but never fires a gate, sends a message, or mutates a\nmember: writes come back `simulated` / `would_*`. Use a test key to explore\nsafely; switch to a `nimbio_live_*` key only when you want real side effects.\n\n**Community keys** — the `/v1/community/*` endpoints require a community-scoped\nkey (created by a community manager in the admin portal). The `/v1/me` endpoint\nworks with any key.\n",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "auth": {
    "type": "bearer",
    "bearer": [
      {
        "key": "token",
        "value": "{{api_key}}",
        "type": "string"
      }
    ]
  },
  "variable": [
    {
      "key": "base_url",
      "value": "https://api.nimbio.com"
    },
    {
      "key": "api_key",
      "value": ""
    },
    {
      "key": "key_id",
      "value": ""
    },
    {
      "key": "latch_id",
      "value": ""
    },
    {
      "key": "community_key_id",
      "value": ""
    },
    {
      "key": "account_community_id",
      "value": ""
    },
    {
      "key": "request_id",
      "value": ""
    },
    {
      "key": "x_request_id",
      "value": ""
    }
  ],
  "item": [
    {
      "name": "Account",
      "item": [
        {
          "name": "Who am I",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/me",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "me"
              ]
            },
            "description": ""
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is < 400', function () {",
                  "  pm.expect(pm.response.code).to.be.below(400);",
                  "});",
                  "var rid = pm.response.headers.get('X-Request-Id');",
                  "if (rid) pm.collectionVariables.set('x_request_id', rid);"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Community · Reads",
      "item": [
        {
          "name": "Get gate status",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/community/gate-status",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "community",
                "gate-status"
              ]
            },
            "description": "Latest known sensed state per latch in the key's community."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is < 400', function () {",
                  "  pm.expect(pm.response.code).to.be.below(400);",
                  "});",
                  "var rid = pm.response.headers.get('X-Request-Id');",
                  "if (rid) pm.collectionVariables.set('x_request_id', rid);",
                  "try {",
                  "  var d = pm.response.json();",
                  "  if (d && d[\"latches\"] && d[\"latches\"][0]) pm.collectionVariables.set('latch_id', String(d[\"latches\"][0][\"latch_id\"]));",
                  "} catch (e) {}"
                ]
              }
            }
          ]
        },
        {
          "name": "Get key + latch statuses",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/community/key-statuses",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "community",
                "key-statuses"
              ]
            },
            "description": "All key + latch statuses (disabled, offline, held_open, disabled_until)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is < 400', function () {",
                  "  pm.expect(pm.response.code).to.be.below(400);",
                  "});",
                  "var rid = pm.response.headers.get('X-Request-Id');",
                  "if (rid) pm.collectionVariables.set('x_request_id', rid);",
                  "try {",
                  "  var d = pm.response.json();",
                  "  if (d && d[\"keys\"] && d[\"keys\"][0]) pm.collectionVariables.set('community_key_id', String(d[\"keys\"][0][\"id\"]));",
                  "} catch (e) {}"
                ]
              }
            }
          ]
        },
        {
          "name": "List keys with restrictions",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/community/keys",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "community",
                "keys"
              ]
            },
            "description": "List every key in the key's community with its access restrictions.\n\nRestriction-focused companion to `/v1/community/key-statuses`: that endpoint\nreports live state (offline, held-open) for monitoring; this one reports the\nconfigured limits on each key — whether it is disabled, time-restricted\n(temporal windows), expiring (`expiry`), depth-limited for resharing\n(`sharing.max_share_depth`), or per-latch restricted (scan-only, NFC-scan\nrequired, a limited number of uses)."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is < 400', function () {",
                  "  pm.expect(pm.response.code).to.be.below(400);",
                  "});",
                  "var rid = pm.response.headers.get('X-Request-Id');",
                  "if (rid) pm.collectionVariables.set('x_request_id', rid);"
                ]
              }
            }
          ]
        },
        {
          "name": "List members",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/community/members",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "community",
                "members"
              ]
            },
            "description": "Pending / accepted / removed members for the key's community."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is < 400', function () {",
                  "  pm.expect(pm.response.code).to.be.below(400);",
                  "});",
                  "var rid = pm.response.headers.get('X-Request-Id');",
                  "if (rid) pm.collectionVariables.set('x_request_id', rid);",
                  "try {",
                  "  var d = pm.response.json();",
                  "  if (d && d[\"accepted\"] && d[\"accepted\"][0]) pm.collectionVariables.set('account_community_id', String(d[\"accepted\"][0][\"account_community_id\"]));",
                  "} catch (e) {}"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Community · Writes",
      "item": [
        {
          "name": "Issue an open",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/community/latches/{{latch_id}}/open",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "community",
                "latches",
                "{{latch_id}}",
                "open"
              ]
            },
            "description": "Issue an open on a latch in the key's community.\n\nTest mode: validate scope, write `test_simulated`, return 200 — box never fires.\nLive mode: synchronous — blocks until the open settles, then returns 200 when\nthe box confirms it opened, 403 when the open is denied, or 504 when the gate\ndoes not confirm within the timeout (`did_not_open`).",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"note\": \"Front gate test\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is < 400', function () {",
                  "  pm.expect(pm.response.code).to.be.below(400);",
                  "});",
                  "var rid = pm.response.headers.get('X-Request-Id');",
                  "if (rid) pm.collectionVariables.set('x_request_id', rid);"
                ]
              }
            }
          ]
        },
        {
          "name": "Add a member by phone",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/community/members",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "community",
                "members"
              ]
            },
            "description": "Add a member to the community by phone number and grant them keys.\n\nIf the phone number has no Nimbio account, a pending account is created.\n`key_ids` are community key ids (from /v1/community/key-statuses); each\ngrants the member access via a new key on their account.\n\nTest mode: validate phone + key scope, write `test_simulated`, return 200 —\nno account is created and no keys are granted.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"key_ids\": [\n    \"{{community_key_id}}\"\n  ],\n  \"phone_number\": \"+15551234567\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is < 400', function () {",
                  "  pm.expect(pm.response.code).to.be.below(400);",
                  "});",
                  "var rid = pm.response.headers.get('X-Request-Id');",
                  "if (rid) pm.collectionVariables.set('x_request_id', rid);",
                  "try {",
                  "  var d = pm.response.json();",
                  "  if (d && d[\"accepted\"] && d[\"accepted\"][0]) pm.collectionVariables.set('account_community_id', String(d[\"accepted\"][0][\"account_community_id\"]));",
                  "} catch (e) {}"
                ]
              }
            }
          ]
        },
        {
          "name": "Grant keys to a member",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/community/members/{{account_community_id}}/grant-keys",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "community",
                "members",
                "{{account_community_id}}",
                "grant-keys"
              ]
            },
            "description": "Grant additional keys to an existing accepted member.\n\n`key_ids` are community key ids (from /v1/community/key-statuses). Each that\nthe member does not already have is granted; a previously disabled key is\nre-enabled; keys they already hold are left unchanged. Use POST\n/v1/community/members to add a brand-new member.\n\nTest mode: validate scope, write `test_simulated`, return 200 — no change.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"key_ids\": [\n    \"{{community_key_id}}\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is < 400', function () {",
                  "  pm.expect(pm.response.code).to.be.below(400);",
                  "});",
                  "var rid = pm.response.headers.get('X-Request-Id');",
                  "if (rid) pm.collectionVariables.set('x_request_id', rid);"
                ]
              }
            }
          ]
        },
        {
          "name": "Disable / enable a member's keys",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/community/members/{{account_community_id}}/keys-disabled",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "community",
                "members",
                "{{account_community_id}}",
                "keys-disabled"
              ]
            },
            "description": "Disable or enable a member's keys (reversible — keys are not removed).\n\n`key_ids` are community key ids (from /v1/community/key-statuses). `disabled`\nis required: true disables the member's keys, false re-enables them.\n\nTest mode: validate scope, write `test_simulated`, return 200 — no change.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"disabled\": true,\n  \"key_ids\": [\n    \"{{community_key_id}}\"\n  ]\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is < 400', function () {",
                  "  pm.expect(pm.response.code).to.be.below(400);",
                  "});",
                  "var rid = pm.response.headers.get('X-Request-Id');",
                  "if (rid) pm.collectionVariables.set('x_request_id', rid);"
                ]
              }
            }
          ]
        },
        {
          "name": "Revoke keys from a member",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/community/members/{{account_community_id}}/revoke-keys",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "community",
                "members",
                "{{account_community_id}}",
                "revoke-keys"
              ]
            },
            "description": "Revoke community keys from a member.\n\n`key_ids` are community key ids (from /v1/community/key-statuses). By\ndefault only those keys are removed and the member stays active; pass\n`remove_member: true` to remove the member from the community entirely.\n\nTest mode: validate scope, write `test_simulated`, return 200 — no change.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"key_ids\": [\n    \"{{community_key_id}}\"\n  ],\n  \"remove_member\": false\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is < 400', function () {",
                  "  pm.expect(pm.response.code).to.be.below(400);",
                  "});",
                  "var rid = pm.response.headers.get('X-Request-Id');",
                  "if (rid) pm.collectionVariables.set('x_request_id', rid);"
                ]
              }
            }
          ]
        },
        {
          "name": "Message all members",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/community/messages",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "community",
                "messages"
              ]
            },
            "description": "Send a message to all community members.\n\nTest mode: validated via dry_run — no push is sent. Live mode: delivered.",
            "body": {
              "mode": "raw",
              "raw": "{\n  \"message\": \"Pool maintenance Thursday 9am–noon.\"\n}",
              "options": {
                "raw": {
                  "language": "json"
                }
              }
            }
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is < 400', function () {",
                  "  pm.expect(pm.response.code).to.be.below(400);",
                  "});",
                  "var rid = pm.response.headers.get('X-Request-Id');",
                  "if (rid) pm.collectionVariables.set('x_request_id', rid);"
                ]
              }
            }
          ]
        }
      ]
    },
    {
      "name": "Community · Logs",
      "item": [
        {
          "name": "Community access log",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/community/access-logs?page=0",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "community",
                "access-logs"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "0",
                  "disabled": false
                }
              ]
            },
            "description": "The community's full access log (gate opens) for the last 90 days,\npaginated 1000 per page (`page` is 0-indexed; `has_more` says whether\nanother page exists). Each row includes the opener's `phone` when on file.\nCommercial communities name the actual opener; residential communities use a\n'<owner> Keychain' grouping. Requires the community's Access Log History on."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is < 400', function () {",
                  "  pm.expect(pm.response.code).to.be.below(400);",
                  "});",
                  "var rid = pm.response.headers.get('X-Request-Id');",
                  "if (rid) pm.collectionVariables.set('x_request_id', rid);"
                ]
              }
            }
          ]
        },
        {
          "name": "Gate status-change log",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/community/gate-status-log?page=0",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "community",
                "gate-status-log"
              ],
              "query": [
                {
                  "key": "page",
                  "value": "0",
                  "disabled": false
                }
              ]
            },
            "description": "The community's gate status-change log (physical open/closed transitions)\nfor the last 90 days, paginated 1000 per page (`page` 0-indexed; `has_more`\nflags another page). Distinct from /v1/community/gate-status (current state).\nRequires the community's Access Log History on."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is < 400', function () {",
                  "  pm.expect(pm.response.code).to.be.below(400);",
                  "});",
                  "var rid = pm.response.headers.get('X-Request-Id');",
                  "if (rid) pm.collectionVariables.set('x_request_id', rid);"
                ]
              }
            }
          ]
        },
        {
          "name": "Member access logs",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/community/members/{{account_community_id}}/access-logs?window=last_30",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "community",
                "members",
                "{{account_community_id}}",
                "access-logs"
              ],
              "query": [
                {
                  "key": "window",
                  "value": "last_30",
                  "disabled": false
                }
              ]
            },
            "description": "A member's access logs (key opens) for a 30-day window.\n\n`window` selects the range: `last_30` (default, the last 30 days), `30_60`\n(30-60 days ago), or `60_90` (60-90 days ago). Scope is the member's key(s)\nplus every descendant key (people they re-shared with); each row names the\nactual opener. Requires the community's Access Log History setting to be on.\nReads behave identically for test and live keys."
          },
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "pm.test('status is < 400', function () {",
                  "  pm.expect(pm.response.code).to.be.below(400);",
                  "});",
                  "var rid = pm.response.headers.get('X-Request-Id');",
                  "if (rid) pm.collectionVariables.set('x_request_id', rid);"
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}
