{
  "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**Account vs community keys** — there are two key types. The `/v1/account/*`\nendpoints (your keys and opening your own latches) require an account-scoped key;\nthe `/v1/community/*` endpoints require a community-scoped key (created by a\ncommunity manager in the admin portal). `/v1/me` works with either — its `key.type`\nfield tells you which one you're holding.\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": "List your keys",
          "request": {
            "method": "GET",
            "header": [],
            "url": {
              "raw": "{{base_url}}/v1/account/keys?include_hidden=False",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "account",
                "keys"
              ],
              "query": [
                {
                  "key": "include_hidden",
                  "value": "False",
                  "disabled": false
                }
              ]
            },
            "description": "Every Nimbio key on your account, with its latches nested. Hidden keys are\nexcluded unless `include_hidden=true`."
          },
          "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": "Open one of your latches",
          "request": {
            "method": "POST",
            "header": [
              {
                "key": "Content-Type",
                "value": "application/json"
              }
            ],
            "url": {
              "raw": "{{base_url}}/v1/account/keys/{{key_id}}/latches/{{latch_id}}/open",
              "host": [
                "{{base_url}}"
              ],
              "path": [
                "v1",
                "account",
                "keys",
                "{{key_id}}",
                "latches",
                "{{latch_id}}",
                "open"
              ]
            },
            "description": "Open a latch using one of your own keys.\n\nTest mode: writes `test_simulated`, returns 200 with `would_open` — box never\nfires (the latch permission is authorized on the backend only for live opens).\nLive mode: synchronous — blocks until the open settles, then returns 200 when\nthe box confirms, 403 when denied, 409 when the latch requires an NFC scan, or\n504 when the gate does 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": "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);"
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}
