Developers · API reference

Register a business without leaving your app.

For fintechs, banks and any software that onboards businesses. Price a CAC registration with your own margin built in, take payment on your checkout — or leave the collecting to innkorp — and hand the order over. We facilitate the registration and hand back the documents.

Overview

You collect the business details, 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.

  • Catalog. Read the business types and live prices so your selection UI is never hardcoded. Prices already include your contract adjustment.
  • Hand off. After a successful payment on your checkout, post the paid order.
  • Track. Listen for webhooks and read the order through to completion.
Base URL https://api.innkorp.com. All requests and responses are JSON.

Authentication

You get two keys from the Partner portal. Treat the secret key like a password, server-side only.

These endpoints are server-to-server and enforce it. A call from a browser page is refused with 403 before anything else is checked, and no Access-Control-Allow-Origin is returned to a foreign origin. Both keys belong on your backend — a browser call means one of them has already reached your users. The one browser that is allowed is our own API explorer, which is served from this origin.

Keys
pk_live_…publishablerequired

Sent in X-innKorp-Key on every call. Treat it as a credential and keep it server-side.

sk_live_…secretrequired

Never sent. It derives the cipher key for the endpoints that seal their payload. Keep it server-side.

Sandbox

A request made with a pk_test_… / sk_test_… key runs against the innkorp sandbox, a separate database with test data, so nothing touches production. Endpoints and payloads are identical; only the prefix differs.

Generate your sandbox keys from the API Keys tab in the Partner portal. Like the live secret, the test secret is shown once when it is generated, so save it then. Regenerating reveals a new one and retires the old.

// Every call carries your API key. The secret is never sent.
const res = await fetch("https://api.innkorp.com/api/v1/pricing/catalog", {
  headers: { "X-innKorp-Key": API_KEY }
});

Encrypting payloads

Two endpoints take a sealed body instead of plain JSON: POST /api/v1/registrations and GET /api/v1/vault/{business_identifier}. You seal the request; we seal the response.

Envelope
v1.<iv>.<tag>.<ciphertext>

AES-256-GCM. Cipher key is SHA-256(secret_key). A fresh 12-byte IV per request and the 16-byte auth tag, each base64url, joined with dots after the version prefix.

Request
X-innKorp-Keystringrequired

Your API key (pk_live_… or pk_test_…). Sent on every call. Keep it server-side.

encryptedstringrequired

The sealed envelope, and the only key in the request body.

time_stampstringrequired

ISO timestamp, placed INSIDE the payload before sealing. Rejected beyond five minutes. innkorp stamps the same field on the payloads it seals back to you.

import crypto from "crypto";

function deriveKey(secretKey) {
  return crypto.createHash("sha256").update(secretKey).digest();
}

function seal(payload, secretKey) {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv("aes-256-gcm", deriveKey(secretKey), iv);
  // Required. Rejected beyond five minutes.
  const body = JSON.stringify({
    ...payload,
    time_stamp: new Date().toISOString()
  });
  const ct = Buffer.concat([cipher.update(body, "utf8"), cipher.final()]);
  return [
    "v1",
    iv.toString("base64url"),
    cipher.getAuthTag().toString("base64url"),
    ct.toString("base64url")
  ].join(".");
}

const res = await fetch("https://api.innkorp.com/api/v1/registrations", {
  method: "POST",
  headers: {
    "X-innKorp-Key": API_KEY,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ encrypted: seal(registration, SECRET_KEY) })
});

Catalog

GET/api/v1/pricing/catalog

Every registrable business type with the price already embedded on it, under your contract. A business_type value here is exactly what you send when you hand off the order. A business type — plus a share capital tier, where one is needed — identifies a price, and that price sits on the item itself, so there is no separate lookup.

Types where needs_share_capital is true carry a prices array instead of a single price — one entry per tier, each naming its own share_capital, in ascending order. That array is the tier list for that type, so there is no separate one to cross-reference. Every option innkorp lists is priced, so you never have to handle a missing one.

Each price comes split: partners_share is your margin under your contract and innkorp_share is innkorp's cut, always summing to price. Charge and hand off price.

Headers
X-innKorp-Keystringrequired

Your API key (pk_live_… or pk_test_…). Sent on every call. Keep it server-side.

const res = await fetch("https://api.innkorp.com/api/v1/pricing/catalog", {
  headers: { "X-innKorp-Key": API_KEY }
});
const { business_types } = await res.json();

// No share capital needed:
// businessType.price

// Needs share capital:
// businessType.prices.find(
//   p => p.share_capital === shareCapital
// ).price
Response200
{
  "business_types": [
    {
      "value": "partnership",
      "label": "Partnership",
      "needs_share_capital": false,
      "price": 45000,
      "partners_share": 5000,
      "innkorp_share": 40000,
      "prices": null
    },
    {
      "value": "private_company_limited_by_shares",
      "label": "Private Company Limited By Shares",
      "needs_share_capital": true,
      "price": null,
      "partners_share": null,
      "innkorp_share": null,
      "prices": [
        {
          "share_capital": 1000000,
          "price": 55000,
          "partners_share": 5000,
          "innkorp_share": 50000
        },
        {
          "share_capital": 2000000,
          "price": 85000,
          "partners_share": 5000,
          "innkorp_share": 80000
        },
        {
          "share_capital": 5000000,
          "price": 175000,
          "partners_share": 5000,
          "innkorp_share": 170000
        }
      ]
    }
  ]
}

Hand off the order

POST/api/v1/registrations

Called from your server after a successful payment. Payment-gated: rejected unless payment.status is success, the amount matches the price the catalog returned for that business type, and an NDPR consent attestation is present.

That assumes you collect the payment yourself, which today is the only supported route — so payment is always required. Handing the order off without it, and letting innkorp collect from the customer instead, is not available yet.

The consent object is an attestation: innkorp cannot verify that consent happened, only that you stated it did. What you send is stored verbatim on the order and is the entire NDPR audit trail for that registration, so all four fields are required — an attestation that cannot say when consent was taken or which notice was shown will not answer a regulator later.

This request carries personal data, so the body is sealed. See Encrypting payloads.

Headers
X-innKorp-Keystringrequired

Your API key (pk_live_… or pk_test_…). Sent on every call. Keep it server-side.

Content-Typestringrequired

Always application/json.

Body Params, inside the envelope
business_identifierstringrequired

Your own id for this business. Echoed on the response, on every registration webhook and on the status read, so you never need a table mapping your ids to ours — and it is what you look the business up by later. It identifies a BUSINESS, not a customer: one customer handing off three businesses sends three different values. Must be unused by you, or the call is rejected with 409. It becomes the URL you read the business back from, so it must be 128 characters or fewer, contain no whitespace and none of / ? # %, and must not be pull or documents — a 400 otherwise.

business_typestringrequired

The business type value from the catalog, e.g. partnership. innKorp derives the classification from it.

share_capitalnumberoptional

Required only for a company by shares.

proposed_business_namestringrequired

The name the customer wants to register.

contact_first_namestringrequired

First name of the person innKorp deals with about this registration.

contact_last_namestringrequired

Last name of that contact.

contact_phone_numberstringrequired

The contact's phone, and the owner's identity anchor: the number they later sign in to innkorp with, and how a returning customer is matched. Either this or contact_email is required.

contact_emailstringrequired

The contact's email, and the other identity anchor. Either this or contact_phone_number is required.

payment.statusstringrequired

Must be "success" or the call is rejected with 402.

payment.referencestringrequired

Your payment reference, and the idempotency key. Sending the same one twice returns the original registration instead of creating a second.

payment.amountnumberrequired

Must match the catalog price, or the call is rejected with 409.

payment.currencystringrequired

Currently only "NGN".

consent.obtainedbooleanrequired

Must be true or the call is rejected with 400.

consent.time_stampstringrequired

ISO 8601 timestamp of when the NDPR consent was obtained. Distinct from the envelope's own time_stamp, which is a replay guard. Rejected with 400 if missing or unparseable.

consent.notice_versionstringoptional

Version of the privacy notice shown at consent time. Optional — send it if you version your notices, omit it if you do not. Stored verbatim either way.

consent.channelstringrequired

Where consent was captured, e.g. "your_app". Rejected with 400 if missing.

// seal() is defined under "Encrypting payloads".
const registration = {
  business_identifier: "biz_8812",   // YOUR id for this business
  business_type: "partnership",
  share_capital: null,
  proposed_business_name: "Adunni Foods",

  contact_first_name: "Adunni",
  contact_last_name: "Okafor",
  contact_phone_number: "+2348012345678",
  contact_email: "adunni@example.com",

  payment: {
    status: "success",
    reference: "PAY-123",
    amount: businessType.price,
    currency: "NGN"
  },
  consent: {
    obtained: true,
    time_stamp: new Date().toISOString(),
    notice_version: "2026-06-01",
    channel: "your_app"
  }
};

const res = await fetch("https://api.innkorp.com/api/v1/registrations", {
  method: "POST",
  headers: {
    "X-innKorp-Key": API_KEY,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({ encrypted: seal(registration, SECRET_KEY) })
});
Response201
{
  "business_identifier": "biz_8812",
  "registration_id": "8f3a1c2b-9d4e-4f1a-b2c3-000000000000",
  "sme_id": "a1b2c3d4-5e6f-7a8b-9c0d-000000000000",
  "status": "received",
  "innkorp_share": 45000,
  "settlement_due_at": "2026-08-06T00:00:00.000Z"
}

Pull business details

GET/api/v1/vault/{business_identifier}

Everything innkorp holds for a registered business: its details, directors, shareholders and documents. Addressed by your own business_identifier, the one you sent at handoff, so you never had to store an id of ours.

It also carries what innkorp determined while processing, which you never sent: business_registration_number, tin and the registered category and subcategory.

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. Wait for the registration.documents_ready webhook rather than polling.

The response returns personal data, so it comes back sealed. See Encrypting payloads.

You only get the fields you asked for. Your integration declares which ones it needs, and anything else comes back null — a section you hold no grant in, such as shareholders, comes back null rather than as an empty list. That is data minimisation, not an error: ask innkorp to widen your access if your use case needs more.

Headers
X-innKorp-Keystringrequired

Your API key (pk_live_… or pk_test_…). Sent on every call. Keep it server-side.

Path Params
business_identifierstringrequired

Your own id for the business, the one you sent when you handed it off. Sent in the URL path, not the body.

const res = await fetch(
  "https://api.innkorp.com/api/v1/vault/" + business_identifier,
  { headers: { "X-innKorp-Key": PUBLIC_KEY } }
);

// open() is defined under "Encrypting payloads".
const { encrypted } = await res.json();
const business = open(encrypted, SECRET_KEY);

if (business.status !== "documents_ready") {
  // Not filed yet — everything but status is null. Wait for the webhook.
}
Response200
{
  "status": "documents_ready",
  "business": {
    "tin": "12345678-0001",
    "business_registration_number": "RC1234567",
    "registered_business_name": "Adunni Foods",
    "registered_business_type": "partnership",
    "registered_business_category": "Accommodation and Food Services",
    "registered_business_subcategory": "Operate restaurant and catering services",
    "city": "Lagos",
    "state": "Lagos",
    "country": "Nigeria",
    "registered_business_address": "12 Marina Rd, Lagos"
  },
  "directors": [
    {
      "first_name": "Adunni",
      "last_name": "Okafor",
      "date_of_birth": "1990-04-12",
      "gender": "Female",
      "phone_number": "+2348012345678",
      "occupation": "Product Manager",
      "email_address": "adunni@example.com",
      "nationality": "Nigerian",
      "residential_address": "5 Allen Avenue, Lagos",
      "id_type": "National Identification Number",
      "id_number": "034322323",
      "is_signatory": true,
      "is_shareholder": true
    }
  ],
  "shareholders": [
    {
      "first_name": "Adunni",
      "last_name": "Okafor",
      "percentage_shareholding": 100,
      "is_beneficial_owner": true
    }
  ],
  "documents": [
    {
      "doc_type": "cac_certificate",
      "file_name": "cac-certificate.pdf",
      "signed_url": "https://…/signed?token=…",
      "expiry_date": null,
      "uploaded_at": "2026-06-16T10:00:00.000Z"
    }
  ]
}
Response200
// Before the filing is done:
{
  "status": "processing",
  "business": null,
  "directors": null,
  "shareholders": null,
  "documents": null
}

Webhook events

innkorp POSTs JSON to your configured webhook URL, signed with HMAC-SHA256 in the X-innKorp-Signature header. Each line below says what actually fires the event.

registration.received

Your hand-off succeeded and the paid order was created.

registration.processing

innkorp started processing the CAC registration.

registration.documents_ready

The CAC-issued documents are in the vault. Pull them with GET /api/v1/vault/{business_identifier}.

registration.completed

The registration is fully completed.

settlement.settled

innkorp confirmed your settlement payment was received.

Example delivery
POST {your webhook_url}
X-innKorp-Signature: <hex HMAC-SHA256 of the raw body>

{
  "event": "registration.documents_ready",
  "time_stamp": "2026-06-18T10:00:00.000Z",
  "data": {
    "registration_id": "8f3a1c2b…",
    "sme_id": "a1b2c3d4…",
    "status": "documents_ready"
  }
}
Verify the signature
import crypto from "crypto";

function verify(rawBody, signatureHeader, webhookSecret) {
  const expected = crypto
    .createHmac("sha256", webhookSecret)
    .update(rawBody)
    .digest("hex");
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}

Document types

doc_type is one of the following. A business may not have every type.

cac_certificatecac_status_reportmemarttin_certificateutility_billboard_resolutionscuml_certificateubo_declarationoperating_licensedirector_idother

Errors

Errors return { "error": "message" } with a standard HTTP status.

400Bad request. A required field is missing, or a sealed payload could not be opened or has expired.
401Missing or invalid API key.
402Payment is not marked successful, so the order cannot be created.
403Called from a browser page, or your account is disabled or not configured for API registrations.
404The registration or business was not found.
409The amount paid does not match the catalog price, or that business_identifier has already been handed off.
500Something went wrong on our side. Safe to retry.

Resources

Ready to integrate?

Create a partner account to get your keys and start registering businesses today.

Get API keys