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.
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.
pk_live_…publishablerequiredSent in X-innKorp-Key on every call. Treat it as a credential and keep it server-side.
sk_live_…secretrequiredNever sent. It derives the cipher key for the endpoints that seal their payload. Keep it server-side.
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.
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.
X-innKorp-KeystringrequiredYour API key (pk_live_… or pk_test_…). Sent on every call. Keep it server-side.
encryptedstringrequiredThe sealed envelope, and the only key in the request body.
time_stampstringrequiredISO 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
/api/v1/pricing/catalogEvery 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.
X-innKorp-KeystringrequiredYour 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{
"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
/api/v1/registrationsCalled 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.
X-innKorp-KeystringrequiredYour API key (pk_live_… or pk_test_…). Sent on every call. Keep it server-side.
Content-TypestringrequiredAlways application/json.
business_identifierstringrequiredYour 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_typestringrequiredThe business type value from the catalog, e.g. partnership. innKorp derives the classification from it.
share_capitalnumberoptionalRequired only for a company by shares.
proposed_business_namestringrequiredThe name the customer wants to register.
contact_first_namestringrequiredFirst name of the person innKorp deals with about this registration.
contact_last_namestringrequiredLast name of that contact.
contact_phone_numberstringrequiredThe 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_emailstringrequiredThe contact's email, and the other identity anchor. Either this or contact_phone_number is required.
payment.statusstringrequiredMust be "success" or the call is rejected with 402.
payment.referencestringrequiredYour payment reference, and the idempotency key. Sending the same one twice returns the original registration instead of creating a second.
payment.amountnumberrequiredMust match the catalog price, or the call is rejected with 409.
payment.currencystringrequiredCurrently only "NGN".
consent.obtainedbooleanrequiredMust be true or the call is rejected with 400.
consent.time_stampstringrequiredISO 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_versionstringoptionalVersion 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.channelstringrequiredWhere 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) })
});{
"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
/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.
X-innKorp-KeystringrequiredYour API key (pk_live_… or pk_test_…). Sent on every call. Keep it server-side.
business_identifierstringrequiredYour 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.
}{
"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"
}
]
}// 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.receivedYour hand-off succeeded and the paid order was created.
registration.processinginnkorp started processing the CAC registration.
registration.documents_readyThe CAC-issued documents are in the vault. Pull them with GET /api/v1/vault/{business_identifier}.
registration.completedThe registration is fully completed.
settlement.settledinnkorp confirmed your settlement payment was received.
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"
}
}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_idotherErrors
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
- API explorer · the same endpoints, runnable in the browser. The catalog only — the two sealed endpoints need your secret key, which must never reach a browser.
- Download the OpenAPI spec · import it into your own tooling to generate a client.
- Download the Postman collection · every endpoint, ready to run.
- Partner portal · get your API keys, set your webhook URL, and view your account-specific integration guide.
- Registration pricing · what innkorp charges before your contract adjustment.
Ready to integrate?
Create a partner account to get your keys and start registering businesses today.
Get API keys