{
  "info": {
    "name": "innKorp API (v1)",
    "_postman_id": "innkorp-api-v1",
    "description": "Official innKorp API collection for fintech partners.\n\nYou collect the business details and the payment on your own UI. innKorp prices the registration, takes the paid order server-to-server, facilitates the CAC registration, and creates a vault for the business owner. You settle what innKorp is owed the next business day.\n\n## Getting started\n1. Set the `baseUrl` variable (default http://localhost:3000 — change to your environment, e.g. https://api.innkorp.com).\n2. Set `public_key` from your innKorp Partner portal (/partners → API Keys). It goes in the `X-innKorp-Key` header on every request; the secret key is never sent.\n3. Run the requests in order: **Pricing → Catalog**, **Registrations → Hand off**, then **Vault → Pull business details**. Ids are saved to collection variables as you go.\n\n## Scope\nThis collection covers the registration API. The embeddable widget flow is driven by innkorp.js in the browser rather than called directly, so it is not included here.\n\n## Server-to-server only\nEvery request here is refused with 403 if it carries a browser Origin. Postman is not a browser and is unaffected, but the same call from page JavaScript will fail — both keys belong on your backend. An interactive OpenAPI explorer is at /developers/api-explorer, and the spec at /innkorp-openapi.json.\n\n## Security\n- Both keys are server-to-server ONLY. Never ship either to a browser.\n- `public_key` goes in `X-innKorp-Key` on every request. `secret_key` is never sent; it only derives the cipher key for sealed payloads.\n- Document URLs in responses are short-lived (1h) signed URLs.\n- Webhooks are signed with HMAC-SHA256 in the `X-innKorp-Signature` header.\n\n## Payload encryption\nEvery request authenticates the same way, with `X-innKorp-Key`. The two endpoints that carry personal data (`POST /registrations` and `GET /vault/{business_identifier}`) additionally take a body sealed under your secret rather than plain JSON: `{ \"encrypted\": \"v1.<iv>.<tag>.<ciphertext>\" }`. The secret itself is never sent.\n\nPostman's sandbox has no AES-256-GCM, so those two requests cannot build their own envelope. They are included for the URL, headers and shape; generate the envelope in your own code (see the Node example at /developers) and paste it in. Every other request in this collection runs as-is.",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "variable": [
    { "key": "baseUrl", "value": "http://localhost:3000", "type": "string" },
    { "key": "public_key", "value": "pk_live_xxx", "type": "string" },
    { "key": "secret_key", "value": "sk_live_xxx", "type": "string" },
    { "key": "sme_id", "value": "", "type": "string" },
    { "key": "business_identifier", "value": "biz_8812", "type": "string" }
  ],
  "item": [
    {
      "name": "Pricing",
      "description": "The registrable business types and what each one costs. Each carries its price (or, if it needs share capital, one priced entry per tier) already adjusted for your contract, split into your share and innkorp's.",
      "item": [
        {
          "name": "Get catalog",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const json = pm.response.json();",
                  "pm.test('200 OK', () => pm.response.code === 200);",
                  "pm.test('has business_types', () => Array.isArray(json.business_types));",
                  "pm.test('business types carry a price', () => {",
                  "  const bt = json.business_types || [];",
                  "  return bt.length > 0 && bt.every(t => 'price' in t && 'prices' in t);",
                  "});",
                  "pm.test('price splits into the two shares', () => {",
                  "  const rows = (json.business_types || []).flatMap(t => t.prices || [t]);",
                  "  return rows.every(r => r.price !== null)",
                  "    && rows.every(r => r.innkorp_share + r.partners_share === r.price);",
                  "});",
                  "console.log('Business types:', (json.business_types || []).map(t => t.value).join(', '));"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [
              { "key": "X-innKorp-Key", "value": "{{public_key}}" }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/pricing/catalog",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "pricing", "catalog"]
            },
            "description": "Every registrable business type in one flat `business_types` list, with the price already on it under your contract. Types where `needs_share_capital` is true carry `prices` — one entry per `share_capital` tier — instead of a single `price`. Each price is split into `partners_share` (your margin) and `innkorp_share`, which always sum to `price`; charge and hand off `price`. Every option listed is priced. Run this first."
          }
        }
      ]
    },
    {
      "name": "Registrations",
      "description": "Hand off the paid order after a successful payment on your checkout, then track it to completion.",
      "item": [
        {
          "name": "Hand off a paid registration",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const json = pm.response.json();",
                  "if (json.registration_id) {",
                  "  pm.collectionVariables.set('registration_id', json.registration_id);",
                  "  console.log('Saved registration_id:', json.registration_id);",
                  "}",
                  "if (json.sme_id) {",
                  "  pm.collectionVariables.set('sme_id', json.sme_id);",
                  "  console.log('Saved sme_id:', json.sme_id);",
                  "}",
                  "pm.test('201 Created', () => pm.response.code === 201);"
                ]
              }
            }
          ],
          "request": {
            "method": "POST",
            "header": [
              { "key": "X-innKorp-Key", "value": "{{public_key}}" },
              { "key": "Content-Type", "value": "application/json" }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/registrations",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "registrations"]
            },
            "body": {
              "mode": "raw",
              "raw": "{\n  \"encrypted\": \"PASTE_YOUR_SEALED_ENVELOPE_HERE\"\n}"
            },
            "description": "Called from YOUR server after a successful payment.\n\nSEALED ENDPOINT. No Authorization header: your publishable key goes in `X-innKorp-Key`, and the body is a single `encrypted` field holding the registration sealed under your secret. Postman cannot build that envelope (no AES-256-GCM in its sandbox), so generate it in your own code and paste it in. The Node `seal()` helper is at /developers under \"Encrypting payloads\".\n\nThe sealed plaintext is:\n\n```\n{\n  \"business_identifier\": \"biz_8812\",\n  \"business_type\": \"partnership\",\n  \"share_capital\": null,\n  \"proposed_business_name\": \"Adunni Foods\",\n  \"contact_first_name\": \"Adunni\",\n  \"contact_last_name\": \"Okafor\",\n  \"contact_phone_number\": \"+2348012345678\",\n  \"contact_email\": \"adunni@example.com\",\n  \"payment\": { \"status\": \"success\", \"reference\": \"PAY-123\", \"amount\": <price from the catalog>, \"currency\": \"NGN\" },\n  \"consent\": { \"obtained\": true, \"time_stamp\": \"<now>\", \"notice_version\": \"2026-06-01\", \"channel\": \"your_app\" },\n  \"time_stamp\": \"<now, ISO. Rejected beyond 5 minutes>\"\n}\n```\n\n`business_identifier` is YOUR id for this business, not for the customer — one customer handing off three businesses sends three different values. It is echoed on the response, on every registration webhook and on the status read, and it is what you look the business up by later. Reusing one you already sent is rejected with 409.\n\n`business_type` is the flat leaf value from the catalog (e.g. \"partnership\") — innKorp derives the classification from it; you never send `classification` or `category`.\n\nYou send NO BVN, date of birth or home address. innKorp collects those from the customer directly once the order lands, so what you hand over is the paid intent plus a person to contact.\n\nNote the two `time_stamp` fields: the top-level one is the envelope's replay guard, and `consent.time_stamp` records when the customer consented. They are close in value but not the same thing.\n\nPayment-gated: rejected with 402 unless `payment.status` is \"success\", 409 unless `payment.amount` matches the catalog's `price` for that business type, 400 unless the full NDPR attestation is present: `consent.obtained` true, `consent.time_stamp` a parseable ISO 8601 value, plus `consent.notice_version` and `consent.channel`. innKorp cannot verify consent happened — the object you send is stored verbatim and is the whole audit trail.\n\n`contact_phone_number` is the business owner's identity anchor: the number they later sign in to innKorp with, and how a returning customer is matched. Send the real one.\n\nSaves `registration_id` and `sme_id` to collection variables."
          }
        }
      ]
    },
    {
      "name": "Vault",
      "description": "What innKorp already holds for a business you registered.",
      "item": [
        {
          "name": "Pull business details",
          "event": [
            {
              "listen": "test",
              "script": {
                "type": "text/javascript",
                "exec": [
                  "const json = pm.response.json();",
                  "pm.test('200 OK', () => pm.response.code === 200);",
                  "pm.test('response is sealed', () => typeof json.encrypted === 'string');",
                  "console.log('Sealed envelope — open it with your secret key to read the payload.');"
                ]
              }
            }
          ],
          "request": {
            "method": "GET",
            "header": [
              { "key": "X-innKorp-Key", "value": "{{public_key}}" }
            ],
            "url": {
              "raw": "{{baseUrl}}/api/v1/vault/{{business_identifier}}",
              "host": ["{{baseUrl}}"],
              "path": ["api", "v1", "vault", "{{business_identifier}}"]
            },
            "description": "Everything innKorp holds for a registered business: its details, directors, shareholders and documents.\n\nAddressed by YOUR OWN `business_identifier` — the one you sent at handoff — so you never had to store an id of ours. An identifier belonging to another partner returns 404, not 403.\n\n**Until `status` is `documents_ready`, only `status` comes back.** `business`, `directors`, `shareholders` and `documents` are all `null`. Nothing has been filed yet, so there is nothing to hand over:\n\n```\n{ \"status\": \"processing\", \"business\": null, \"directors\": null, \"shareholders\": null, \"documents\": null }\n```\n\nSEALED RESPONSE. The body is `{ encrypted }`; open it with the `open()` helper at /developers under \"Encrypting payloads\". Postman cannot decrypt it (no AES-256-GCM in its sandbox).\n\nThe sealed plaintext, once filed, is:\n\n```\n{\n  \"status\": \"documents_ready\",\n  \"business\": {\n    \"tin\": \"12345678-0001\",\n    \"business_registration_number\": \"RC1234567\",\n    \"registered_business_name\": \"Adunni Foods\",\n    \"registered_business_type\": \"partnership\",\n    \"registered_business_category\": \"Accommodation and Food Services\",\n    \"registered_business_subcategory\": \"Operate restaurant and catering services\",\n    \"city\": \"Lagos\",\n    \"state\": \"Lagos\",\n    \"country\": \"Nigeria\",\n    \"registered_business_address\": \"12 Marina Rd, Lagos\"\n  },\n  \"directors\": [{\n    \"first_name\": \"Adunni\", \"last_name\": \"Okafor\",\n    \"date_of_birth\": \"1990-04-12\", \"gender\": \"Female\",\n    \"phone_number\": \"+2348012345678\", \"occupation\": \"Product Manager\",\n    \"email_address\": \"adunni@example.com\", \"nationality\": \"Nigerian\",\n    \"residential_address\": \"5 Allen Avenue, Lagos\",\n    \"id_type\": \"National Identification Number\", \"id_number\": \"034322323\",\n    \"is_signatory\": true, \"is_shareholder\": true\n  }],\n  \"shareholders\": [{\n    \"first_name\": \"Adunni\", \"last_name\": \"Okafor\",\n    \"percentage_shareholding\": 100, \"is_beneficial_owner\": true\n  }],\n  \"documents\": [{\n    \"doc_type\": \"cac_certificate\", \"file_name\": \"cac-certificate.pdf\",\n    \"signed_url\": \"https://…/signed?token=…\", \"expiry_date\": null,\n    \"uploaded_at\": \"2026-06-16T10:00:00.000Z\"\n  }]\n}\n```\n\n**You only get the fields you asked for.** Your integration declares which ones it needs; anything else comes back `null`, and a section you hold no grant in comes back `null` rather than as an empty list. Ask innKorp to widen your access if your use case needs more."
          }
        }
      ]
    }
  ]
}
