{
  "openapi": "3.0.3",
  "info": {
    "title": "innKorp Partner API",
    "version": "1.0.0",
    "description": "Register a business with the CAC from your own app.\n\n**Server-to-server only.** Every endpoint here is refused with 403 when called from a browser page — your API key and secret must never reach client-side code. The one exception is this explorer, which is served from the same origin as the API.\n\nTwo endpoints take a **sealed** body instead of plain JSON: `POST /api/v1/registrations` and `GET /api/v1/vault/{business_identifier}`. The envelope is `v1.<iv>.<tag>.<ciphertext>`, AES-256-GCM with the cipher key derived as `SHA-256(secret_key)`, and it carries a `time_stamp` inside the ciphertext that is rejected beyond ±5 minutes. This explorer cannot build or open one — use it for the URLs, headers and shapes, and seal in your own code."
  },
  "servers": [
    { "url": "https://api.innkorp.com", "description": "Production (pk_live_… keys)" },
    { "url": "http://localhost:3000", "description": "Local development" }
  ],
  "tags": [
    { "name": "Pricing", "description": "What innKorp can register and what it costs under your contract." },
    { "name": "Registrations", "description": "Hand off a paid order." },
    { "name": "Vault", "description": "Read back a registered business." }
  ],
  "security": [{ "ApiKey": [] }],
  "paths": {
    "/api/v1/pricing/catalog": {
      "get": {
        "tags": ["Pricing"],
        "summary": "Catalog",
        "operationId": "getCatalog",
        "description": "Every registrable business type with the price already on it under your contract. A `business_type` value here is exactly what you send when you hand off the order. Every option listed is priced.",
        "responses": {
          "200": {
            "description": "The catalog.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Catalog" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/BrowserCall" }
        }
      }
    },
    "/api/v1/registrations": {
      "post": {
        "tags": ["Registrations"],
        "summary": "Hand off the order",
        "operationId": "createRegistration",
        "description": "Called from YOUR server after a successful payment on your checkout.\n\n**Sealed.** The body is a single `encrypted` field; the plaintext shape is documented on `RegistrationPlaintext` below. You send no BVN, date of birth or home address — innKorp collects those from the customer directly once the order lands.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SealedRequest" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Order created. Replaying the same `payment.reference` returns the original with 200 rather than creating a second.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/HandoffResponse" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "402": {
            "description": "`payment.status` is not \"success\".",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "403": { "$ref": "#/components/responses/BrowserCall" },
          "409": {
            "description": "`payment.amount` does not match the catalog price, or that `business_identifier` has already been handed off.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    },
    "/api/v1/vault/{business_identifier}": {
      "get": {
        "tags": ["Vault"],
        "summary": "Pull business details",
        "operationId": "pullBusiness",
        "description": "Everything innKorp holds for a registered business, addressed by YOUR own id for it.\n\n**Until `status` is `documents_ready`, only `status` comes back** — `business`, `directors`, `shareholders` and `documents` are all null. Nothing has been filed yet.\n\nWhat survives that gate is cut to the fields your integration was granted. Anything else is null, and a section you hold no grant in is null rather than an empty list.\n\n**Sealed response.** The body is `{ encrypted }`; open it with your secret.",
        "parameters": [
          {
            "name": "business_identifier",
            "in": "path",
            "required": true,
            "description": "Your own id for the business, the one you sent at handoff.",
            "schema": { "type": "string", "maxLength": 128, "example": "biz_8812" }
          }
        ],
        "responses": {
          "200": {
            "description": "Sealed payload. Plaintext shape is `PullPlaintext` below.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/SealedResponse" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/BrowserCall" },
          "404": {
            "description": "No business with that identifier for your account. An identifier belonging to another partner is a 404, not a 403.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKey": {
        "type": "apiKey",
        "in": "header",
        "name": "X-innKorp-Key",
        "description": "Your publishable key (pk_live_… or pk_test_…). A pk_test_ key runs against the sandbox database. The SECRET key is never sent — it only derives the cipher key for sealed payloads."
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid X-innKorp-Key.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "BadRequest": {
        "description": "A required field is missing or malformed.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "BrowserCall": {
        "description": "Called from a browser page. These endpoints are server-to-server; a browser call means your key is in client-side code.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": { "error": { "type": "string" } },
        "required": ["error"]
      },
      "SealedRequest": {
        "type": "object",
        "required": ["encrypted"],
        "properties": {
          "encrypted": {
            "type": "string",
            "description": "v1.<iv>.<tag>.<ciphertext>",
            "example": "v1.kxPUEy3GgOa-Gmf4.9M5gEF-cG4vxdcp.ICpHc3AEPWB7ewN..."
          }
        }
      },
      "SealedResponse": {
        "type": "object",
        "required": ["encrypted"],
        "properties": {
          "encrypted": { "type": "string", "description": "v1.<iv>.<tag>.<ciphertext>" }
        }
      },
      "Money": {
        "type": "object",
        "description": "price always equals innkorp_share + partners_share.",
        "properties": {
          "price": { "type": "number", "description": "What the customer pays.", "example": 45000 },
          "partners_share": { "type": "number", "description": "Your margin under your contract.", "example": 5000 },
          "innkorp_share": { "type": "number", "description": "innKorp's cut.", "example": 40000 }
        }
      },
      "TierPrice": {
        "allOf": [
          { "type": "object", "properties": { "share_capital": { "type": "integer", "example": 1000000 } } },
          { "$ref": "#/components/schemas/Money" }
        ]
      },
      "BusinessType": {
        "type": "object",
        "properties": {
          "value": { "type": "string", "example": "partnership", "description": "Send this as business_type at handoff." },
          "label": { "type": "string", "example": "Partnership" },
          "needs_share_capital": { "type": "boolean" },
          "price": { "type": "number", "nullable": true, "description": "Null when needs_share_capital is true — the tiers in `prices` carry it." },
          "partners_share": { "type": "number", "nullable": true },
          "innkorp_share": { "type": "number", "nullable": true },
          "prices": {
            "type": "array",
            "nullable": true,
            "description": "One entry per share capital tier, ascending. Null when the type needs no share capital. This array IS the tier list for the type.",
            "items": { "$ref": "#/components/schemas/TierPrice" }
          }
        }
      },
      "Catalog": {
        "type": "object",
        "properties": {
          "business_types": { "type": "array", "items": { "$ref": "#/components/schemas/BusinessType" } }
        }
      },
      "RegistrationPlaintext": {
        "type": "object",
        "description": "What you seal into `encrypted`. Not sent as JSON.",
        "required": [
          "business_identifier",
          "business_type",
          "proposed_business_name",
          "payment",
          "consent",
          "time_stamp"
        ],
        "properties": {
          "business_identifier": {
            "type": "string",
            "maxLength": 128,
            "example": "biz_8812",
            "description": "YOUR id for this BUSINESS, not for the customer — one customer with three businesses sends three values. Unique per partner. It becomes the URL you read the business back from, so no whitespace or / \\ ? # %, and not \"pull\" or \"documents\"."
          },
          "business_type": { "type": "string", "example": "partnership", "description": "The flat leaf value from the catalog. innKorp derives the classification from it." },
          "share_capital": { "type": "integer", "nullable": true, "description": "Only for a company by shares." },
          "proposed_business_name": { "type": "string", "example": "Adunni Foods" },
          "contact_first_name": { "type": "string", "example": "Adunni" },
          "contact_last_name": { "type": "string", "example": "Okafor" },
          "contact_phone_number": { "type": "string", "example": "+2348012345678", "description": "The owner's identity anchor. Either this or contact_email is required." },
          "contact_email": { "type": "string", "example": "adunni@example.com" },
          "payment": {
            "type": "object",
            "required": ["status", "reference", "amount", "currency"],
            "properties": {
              "status": { "type": "string", "enum": ["success"] },
              "reference": { "type": "string", "example": "PAY-123", "description": "Your reference, and the idempotency key." },
              "amount": { "type": "number", "description": "Must equal the catalog price.", "example": 45000 },
              "currency": { "type": "string", "enum": ["NGN"] }
            }
          },
          "consent": {
            "type": "object",
            "required": ["obtained", "time_stamp", "channel"],
            "properties": {
              "obtained": { "type": "boolean", "enum": [true] },
              "time_stamp": { "type": "string", "format": "date-time", "description": "When the CUSTOMER consented. Distinct from the envelope's time_stamp." },
              "notice_version": { "type": "string", "nullable": true, "description": "Optional. Send it if you version your privacy notices." },
              "channel": { "type": "string", "example": "your_app" }
            }
          },
          "time_stamp": {
            "type": "string",
            "format": "date-time",
            "description": "The envelope's replay stamp, inside the ciphertext. Rejected beyond ±5 minutes."
          }
        }
      },
      "HandoffResponse": {
        "type": "object",
        "properties": {
          "business_identifier": { "type": "string", "example": "biz_8812" },
          "registration_id": { "type": "string", "format": "uuid" },
          "sme_id": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "example": "received" },
          "innkorp_share": { "type": "number", "example": 40000 },
          "settlement_due_at": { "type": "string", "format": "date-time" }
        }
      },
      "PullPlaintext": {
        "type": "object",
        "description": "What `encrypted` opens to. Every section is null until status is documents_ready, and then null for anything your integration was not granted.",
        "properties": {
          "status": { "type": "string", "example": "documents_ready", "description": "Never null, never redacted." },
          "business": {
            "type": "object",
            "nullable": true,
            "properties": {
              "tin": { "type": "string", "nullable": true },
              "business_registration_number": { "type": "string", "nullable": true },
              "registered_business_name": { "type": "string", "nullable": true },
              "registered_business_type": { "type": "string", "nullable": true },
              "registered_business_category": { "type": "string", "nullable": true },
              "registered_business_subcategory": { "type": "string", "nullable": true },
              "city": { "type": "string", "nullable": true },
              "state": { "type": "string", "nullable": true },
              "country": { "type": "string", "nullable": true },
              "registered_business_address": { "type": "string", "nullable": true }
            }
          },
          "directors": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "object",
              "properties": {
                "first_name": { "type": "string", "nullable": true },
                "last_name": { "type": "string", "nullable": true },
                "date_of_birth": { "type": "string", "nullable": true },
                "gender": { "type": "string", "nullable": true },
                "phone_number": { "type": "string", "nullable": true },
                "occupation": { "type": "string", "nullable": true },
                "email_address": { "type": "string", "nullable": true },
                "nationality": { "type": "string", "nullable": true },
                "residential_address": { "type": "string", "nullable": true },
                "id_type": { "type": "string", "nullable": true },
                "id_number": { "type": "string", "nullable": true },
                "is_signatory": { "type": "boolean", "nullable": true },
                "is_shareholder": { "type": "boolean", "nullable": true }
              }
            }
          },
          "shareholders": {
            "type": "array",
            "nullable": true,
            "items": {
              "type": "object",
              "properties": {
                "first_name": { "type": "string", "nullable": true },
                "last_name": { "type": "string", "nullable": true },
                "percentage_shareholding": { "type": "number", "nullable": true },
                "is_beneficial_owner": { "type": "boolean", "nullable": true }
              }
            }
          },
          "documents": {
            "type": "array",
            "nullable": true,
            "description": "Only the document types your integration was granted. A type you were not granted is absent, not null.",
            "items": {
              "type": "object",
              "properties": {
                "doc_type": { "type": "string", "example": "cac_certificate" },
                "file_name": { "type": "string" },
                "signed_url": { "type": "string", "nullable": true, "description": "Short-lived (1h) signed URL." },
                "expiry_date": { "type": "string", "nullable": true },
                "uploaded_at": { "type": "string", "format": "date-time" }
              }
            }
          }
        }
      }
    }
  }
}
