The finpo API

finpo runs the counter, the stock and the theft checks of shops, restaurants, pharmacies and hotels. The API lets your software work with a business’s finpo records: keep an online shop’s stock in step, send orders to the kitchen, read the day’s sales into an accounting package, or know a customer’s visits in a loyalty app.

It is a REST API with JSON in and out. Every object has an object and an id; money is a whole number in the smallest unit of the business’s currency (kobo for naira, so 75000 is ₦750) with the currency beside it; times are ISO 8601. It runs on finpo cloud, beside the records the shop computer syncs.

Sales cannot be written through the API. They are signed where they happen, at the counter, which is what lets finpo check every one of them. Send an order instead: the shop takes it and it becomes a sale.

Base URL
https://finpo.nikcano.com/api/developer/v1

Authentication

Every request carries a key in the Authorization header, as Bearer. The business’s owner makes keys under Settings, Developers, and chooses what each may do.

  • Secret keys (fk_live_..., fk_test_...) are for your server. Keep them like a password; never put one in a browser or an app.
  • Publishable keys (fkp_live_..., fkp_test_...) are made to be seen: put them in a shop’s own website or app. They can only read the catalogue and stock and place orders (business:read, products:read, stock:read, orders:write).

A key is shown once, when it is made. The owner can roll it (a new secret, the old one stops) or revoke it at any time. The scopes a key can have:

  • business:read: See the business and its branches
  • products:read: See products and categories
  • products:write: Add and change products
  • stock:read: See stock levels
  • customers:read: See customers
  • customers:write: Add and change customers
  • sales:read: See sales
  • orders:read: See online orders
  • orders:write: Send online orders to the shop
  • webhooks:write: Manage webhook endpoints
  • costs:read: See cost prices (the owner’s figures)
curl https://finpo.nikcano.com/api/developer/v1/business \
  -H "Authorization: Bearer fk_test_..."

Test mode

A test key reads the business’s real records, within its scopes, so you build against a real catalogue. Every write is checked in full and answered as it would be, with livemode: false, but nothing is saved. Test keys work whether or not the business has a plan, so you can start before the owner pays. Live keys need finpo cloud running for the business.

Errors

A failed request answers with an HTTP status and a body with type, code, a message written for a person, the param at fault when there is one, and a request_id to quote to us. The types are invalid_request_error, authentication_error, permission_error, rate_limit_error and api_error.

  • api_key_missing (401): No key was sent, or it is not in the right form.
  • api_key_invalid (401): The key is not known, or it was revoked or rolled.
  • account_inactive (403): The business is not active on finpo.
  • plan_inactive (403): A live key needs finpo cloud running for the business. Test keys always work.
  • scope_missing (403): The key does not have the scope this needs.
  • secret_key_required (403): A publishable key cannot do this. Use a secret key on your server.
  • route_missing (404): There is no such endpoint.
  • resource_missing (404): No object has that id (or a line names a product or unit that does not exist).
  • parameter_missing (400): A required parameter was not sent.
  • parameter_invalid (400): A parameter has the wrong type or value, or is not a parameter of this request.
  • body_invalid (400): The body is not a JSON object.
  • resource_conflict (409): It clashes with something that exists, such as a customer’s name or a product code.
  • idempotency_key_reused (409): The Idempotency-Key was used with a different request.
  • rate_limited (429): Too many requests for this key. Wait for the Retry-After header’s seconds.
  • internal_error (500): A fault of ours. It is recorded; try again.
Error
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_missing",
    "message": "\"name\" is required.",
    "param": "name",
    "doc_url": "/docs/api#errors-parameter_missing",
    "request_id": "req_9437b3403c9a4e5fb17b"
  }
}

Pagination

Lists come newest first, as { object: "list", data, has_more, url }. Ask for up to 100 with limit (20 by default). For the next page send starting_after with the id of the last object you have; for the page before, ending_before with the id of the first.

A list
{
  "object": "list",
  "data": [
    "..."
  ],
  "has_more": true,
  "url": "/api/developer/v1/products"
}

Idempotent requests

Send an Idempotency-Key header (a UUID is ideal) with any POST. If the network fails and you send it again with the same key, you get the first answer back, with Idempotent-Replayed: true, and nothing is made twice. Keys are kept 24 hours. The same key with a different request is refused.

Versions

The version is in the path, v1. Within it, changes only add: new endpoints, new parameters, new fields. Every answer carries Finpo-Version: 2026-10-12. A change that would break code gets a new dated version, announced in advance.

Rate limits

A live secret key may send 100 requests a minute (20 while the business has no plan running), a test key 60, and a publishable key 30 a minute from each visitor. Each answer carries RateLimit-Limit and RateLimit-Remaining; over the limit you get a 429 with Retry-After in seconds.

Browsers and CORS

The API answers browsers from any origin: every response allows it, and a browser’s preflight is answered at once. No cookie is ever read, so only the key decides what a request may do. In a browser, use a publishable key.

Webhooks

finpo can tell your server the moment something happens, instead of you asking every minute. Add an endpoint in Settings, Developers or with the API, choose its events, and keep the signing secret it shows once.

Each delivery is a POST of { event, at, data } with the headers X-Finpo-Event and X-Finpo-Signature: t=...,v1=..., an HMAC-SHA256 of <t>.<body> with your secret. Refuse a delivery whose signature does not match or whose time is more than five minutes away. A delivery that fails is tried three times. Events: sale.recordedstock.changeditem.upsertedcustomer.upsertedorder.created

Check a signature
import crypto from 'node:crypto';

// header: the X-Finpo-Signature header, as "t=1760264400,v1=5257a8..."
export function isFromFinpo(secret, rawBody, header) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const age = Math.abs(Date.now() / 1000 - Number(parts.t));
  if (!parts.t || !parts.v1 || age > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

MCP for AI tools

finpo runs an MCP server, so an AI assistant can read and work with a business’s records in conversation: what sold most this week, which customers owe money, what is running low. It speaks Streamable HTTP at https://finpo.nikcano.com/api/mcp, protocol 2026-07-28 (stateless, with server/discover), and answers initialize for clients on 2025-11-25.

It uses the same keys, scopes, limits and test mode as the API. Each operation below is a tool of the same name (its MCP tool is named under each one), and a key only sees the tools its scopes allow. It also offers the resources finpo://business and finpo://docs/api, and the prompts daily_summary, restock_list and customer_lookup. Use a test key until you trust the assistant with real writes.

Connect
claude mcp add --transport http finpo https://finpo.nikcano.com/api/mcp \
  --header "Authorization: Bearer fk_test_..."

Business

Retrieve the business

GET /api/developer/v1/business

The business the key belongs to: its name, its account number, the currency its money is in, and what kind of business it is.

Scope: business:read. Secret or publishable keys. MCP tool: retrieve_business.

No parameters.

GET /business
curl https://finpo.nikcano.com/api/developer/v1/business \
  -H "Authorization: Bearer fk_test_..."
Response
{
  "object": "business",
  "id": "cmv2a1b2c0001bus",
  "name": "Adaku Mart",
  "account_number": "85063792",
  "currency": "NGN",
  "kind": "provisions",
  "livemode": true
}
Try it

Locations

List locations

GET /api/developer/v1/locations

The business’s branches: every place it sells from or keeps stock in. Most shops have one.

Scope: business:read. Secret or publishable keys. MCP tool: list_locations.

No parameters.

GET /locations
curl https://finpo.nikcano.com/api/developer/v1/locations \
  -H "Authorization: Bearer fk_test_..."
Response
{
  "object": "list",
  "data": [
    {
      "object": "location",
      "id": "cmv2a1b2c0001loc",
      "name": "Victoria Island",
      "code": "VIC",
      "address": "12 Adeola Odeku Street",
      "is_default": true,
      "active": true
    }
  ],
  "has_more": false,
  "url": "/api/developer/v1/locations"
}
Try it

Categories

List categories

GET /api/developer/v1/categories

The categories products are grouped in, as the shop arranged them.

Scope: products:read. Secret or publishable keys. MCP tool: list_categories.

No parameters.

GET /categories
curl https://finpo.nikcano.com/api/developer/v1/categories \
  -H "Authorization: Bearer fk_test_..."
Response
{
  "object": "list",
  "data": [
    {
      "object": "category",
      "id": "cmv2a1b2c0001cat",
      "name": "Milk and beverages",
      "parent": null
    }
  ],
  "has_more": false,
  "url": "/api/developer/v1/categories"
}
Try it

Products

List products

GET /api/developer/v1/products

The shop’s products, newest first. `query` finds products the way the shop’s own counter does: a barcode, a code, or a name, even typed wrong.

Scope: products:read. Secret or publishable keys. MCP tool: list_products.

Parameters

  • limitintegerin query

    How many to return, 1 to 100. The default is 20.

  • starting_afterstringin query

    The id of the last object of the page before: the next page starts after it.

  • ending_beforestringin query

    The id of the first object of the page after: the page before ends before it.

  • querystringin query

    A barcode, a SKU, or words of the name. Forgiving of spelling.

  • categorystringin query

    Only products in this category.

  • activebooleanin query

    Only products on sale (true) or only those taken off sale (false).

GET /products
curl https://finpo.nikcano.com/api/developer/v1/products?limit=20&query=peak%20milk \
  -H "Authorization: Bearer fk_test_..."
Response
{
  "object": "list",
  "data": [
    {
      "object": "product",
      "id": "cmv2a1b2c0001prd",
      "name": "Peak evaporated milk 160g",
      "sku": "PEAK-160",
      "brand": "Peak",
      "kind": "good",
      "barcodes": [
        "6151100055512"
      ],
      "category": "cmv2a1b2c0001cat",
      "image_url": null,
      "active": true,
      "base_unit": "piece",
      "container_size": 1,
      "container_label": "tin",
      "currency": "NGN",
      "units": [
        {
          "key": "FULL",
          "label": "Tin",
          "quantity": 1,
          "price": 75000,
          "price_min": null,
          "price_max": null
        }
      ],
      "created": "2026-10-12T09:00:00.000Z",
      "updated": "2026-10-12T09:00:00.000Z"
    }
  ],
  "has_more": false,
  "url": "/api/developer/v1/products"
}
Try it

Retrieve a product

GET /api/developer/v1/products/{id}

One product, with every unit it is sold in and its price.

Scope: products:read. Secret or publishable keys. MCP tool: retrieve_product.

Parameters

  • idstringrequiredin path

    The object’s id.

GET /products/{id}
curl https://finpo.nikcano.com/api/developer/v1/products/product_id \
  -H "Authorization: Bearer fk_test_..."
Response
{
  "object": "product",
  "id": "cmv2a1b2c0001prd",
  "name": "Peak evaporated milk 160g",
  "sku": "PEAK-160",
  "brand": "Peak",
  "kind": "good",
  "barcodes": [
    "6151100055512"
  ],
  "category": "cmv2a1b2c0001cat",
  "image_url": null,
  "active": true,
  "base_unit": "piece",
  "container_size": 1,
  "container_label": "tin",
  "currency": "NGN",
  "units": [
    {
      "key": "FULL",
      "label": "Tin",
      "quantity": 1,
      "price": 75000,
      "price_min": null,
      "price_max": null
    }
  ],
  "created": "2026-10-12T09:00:00.000Z",
  "updated": "2026-10-12T09:00:00.000Z"
}
Try it

Create a product

POST /api/developer/v1/products

Adds a product, sold one at a time at one price. It is written exactly as a product added in finpo is, with its signed record, and checked against the plan’s limits. For sizes, cartons and fractions, add the product in finpo itself.

Scope: products:write. Secret keys only. MCP tool: create_product.

Parameters

  • namestringrequired

    What the product is called at the counter.

  • priceintegerrequired

    The price of one, in the smallest unit of the business’s currency (kobo for naira).

  • unit_labelstring

    What one is called: "Tin", "Pack", "Plate". The default is "Each".

  • skustring

    The shop’s own code for it. One is made up when none is sent.

  • barcodesarray of string

    Barcodes printed on it.

  • brandstring

    The maker.

  • categorystring

    The id of a category.

  • kindstring

    A thing on a shelf (the default), a plate on a menu, or a service that holds no stock.

    One of: goodmenuservice

  • costinteger

    What one cost the shop. Only kept when the key has the `costs:read` scope.

  • opening_quantityinteger

    How many the shop has now. The default is none.

  • locationstring

    The location the opening quantity is at. The default is the main one.

POST /products
curl -X POST https://finpo.nikcano.com/api/developer/v1/products \
  -H "Authorization: Bearer fk_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name":"Peak evaporated milk 160g","price":75000,"unit_label":"Tin","sku":"PEAK-160","barcodes":["6151100055512"],"brand":"Peak","opening_quantity":24}'
Response
{
  "object": "product",
  "id": "cmv2a1b2c0001prd",
  "name": "Peak evaporated milk 160g",
  "sku": "PEAK-160",
  "brand": "Peak",
  "kind": "good",
  "barcodes": [
    "6151100055512"
  ],
  "category": "cmv2a1b2c0001cat",
  "image_url": null,
  "active": true,
  "base_unit": "piece",
  "container_size": 1,
  "container_label": "tin",
  "currency": "NGN",
  "units": [
    {
      "key": "FULL",
      "label": "Tin",
      "quantity": 1,
      "price": 75000,
      "price_min": null,
      "price_max": null
    }
  ],
  "created": "2026-10-12T09:00:00.000Z",
  "updated": "2026-10-12T09:00:00.000Z"
}

Stock levels

List stock levels

GET /api/developer/v1/stock_levels

How much of each product is at each location, in the product’s own unit, as counted and confirmed. `pending_change` is what staff recorded and the owner has not approved yet.

Scope: stock:read. Secret or publishable keys. MCP tool: list_stock_levels.

Parameters

  • limitintegerin query

    How many to return, 1 to 100. The default is 20.

  • starting_afterstringin query

    The id of the last object of the page before: the next page starts after it.

  • ending_beforestringin query

    The id of the first object of the page after: the page before ends before it.

  • productstringin query

    Only this product.

  • locationstringin query

    Only this location.

GET /stock_levels
curl https://finpo.nikcano.com/api/developer/v1/stock_levels?limit=20 \
  -H "Authorization: Bearer fk_test_..."
Response
{
  "object": "list",
  "data": [
    {
      "object": "stock_level",
      "id": "cmv2a1b2c0001stk",
      "product": "cmv2a1b2c0001prd",
      "location": "cmv2a1b2c0001loc",
      "quantity": 24,
      "pending_change": 0,
      "containers": 24,
      "unit": "piece",
      "container_label": "tin",
      "container_size": 1,
      "updated": "2026-10-12T09:00:00.000Z"
    }
  ],
  "has_more": false,
  "url": "/api/developer/v1/stock_levels"
}
Try it

Customers

List customers

GET /api/developer/v1/customers

The shop’s customers, newest first, with their visits, points and what they owe. `query` finds a name even typed wrong, or a phone number.

Scope: customers:read. Secret keys only. MCP tool: list_customers.

Parameters

  • limitintegerin query

    How many to return, 1 to 100. The default is 20.

  • starting_afterstringin query

    The id of the last object of the page before: the next page starts after it.

  • ending_beforestringin query

    The id of the first object of the page after: the page before ends before it.

  • querystringin query

    A name or a phone number.

GET /customers
curl https://finpo.nikcano.com/api/developer/v1/customers?limit=20&query=Ngozi \
  -H "Authorization: Bearer fk_test_..."
Response
{
  "object": "list",
  "data": [
    {
      "object": "customer",
      "id": "cmv2a1b2c0001cus",
      "name": "Ngozi Eze",
      "phone": "+2348031234567",
      "email": null,
      "visits": 12,
      "points": 340,
      "currency": "NGN",
      "balance_owed": 0,
      "lifetime_spend": 4820000,
      "last_visit": "2026-10-11T17:20:00.000Z",
      "created": "2026-03-02T10:00:00.000Z"
    }
  ],
  "has_more": false,
  "url": "/api/developer/v1/customers"
}
Try it

Retrieve a customer

GET /api/developer/v1/customers/{id}

One customer by id: their name and contacts, how often they come and what they have spent, their points, and what they owe the shop.

Scope: customers:read. Secret keys only. MCP tool: retrieve_customer.

Parameters

  • idstringrequiredin path

    The object’s id.

GET /customers/{id}
curl https://finpo.nikcano.com/api/developer/v1/customers/customer_id \
  -H "Authorization: Bearer fk_test_..."
Response
{
  "object": "customer",
  "id": "cmv2a1b2c0001cus",
  "name": "Ngozi Eze",
  "phone": "+2348031234567",
  "email": null,
  "visits": 12,
  "points": 340,
  "currency": "NGN",
  "balance_owed": 0,
  "lifetime_spend": 4820000,
  "last_visit": null,
  "created": "2026-03-02T10:00:00.000Z"
}
Try it

Create a customer

POST /api/developer/v1/customers

Adds a customer. A business keeps one customer per name, so a name already on the list is refused with `resource_conflict`.

Scope: customers:write. Secret keys only. MCP tool: create_customer.

Parameters

  • namestringrequired

    Their name.

  • phonestring

    Their phone number, which is how the shop finds them again.

  • notesstring

    Anything the shop should know.

POST /customers
curl -X POST https://finpo.nikcano.com/api/developer/v1/customers \
  -H "Authorization: Bearer fk_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"name":"Ngozi Eze","phone":"08031234567"}'
Response
{
  "object": "customer",
  "id": "cmv2a1b2c0001cus",
  "name": "Ngozi Eze",
  "phone": "08031234567",
  "email": null,
  "visits": 0,
  "points": 0,
  "currency": "NGN",
  "balance_owed": 0,
  "lifetime_spend": 0,
  "last_visit": null,
  "created": "2026-10-12T09:00:00.000Z"
}

Sales

List sales

GET /api/developer/v1/sales

Sales recorded at the counter and on the shop computer, newest first, with their lines and payments. Sales cannot be written through the API: they are signed where they happen, which is what lets finpo check them.

Scope: sales:read. Secret keys only. MCP tool: list_sales.

Parameters

  • limitintegerin query

    How many to return, 1 to 100. The default is 20.

  • starting_afterstringin query

    The id of the last object of the page before: the next page starts after it.

  • ending_beforestringin query

    The id of the first object of the page after: the page before ends before it.

  • day_fromstringin query

    The first business day, as YYYY-MM-DD.

  • day_tostringin query

    The last business day, as YYYY-MM-DD.

  • locationstringin query

    Only sales at this location.

GET /sales
curl https://finpo.nikcano.com/api/developer/v1/sales?limit=20&day_from=2026-10-01 \
  -H "Authorization: Bearer fk_test_..."
Response
{
  "object": "list",
  "data": [
    {
      "object": "sale",
      "id": "cmv2a1b2c0001sal",
      "receipt_number": "VIC-000018",
      "location": "cmv2a1b2c0001loc",
      "business_day": "2026-10-12",
      "created": "2026-10-12T13:05:00.000Z",
      "currency": "NGN",
      "total": 700000,
      "gross": 700000,
      "customer": null,
      "voided": false,
      "refunded": false,
      "lines": [
        {
          "product": "cmv2a1b2c0001prd",
          "unit": "PLATE",
          "quantity": 2,
          "unit_price": 350000,
          "list_price": 350000
        }
      ],
      "payments": [
        {
          "method": "cash",
          "amount": 700000,
          "reference": null
        }
      ]
    }
  ],
  "has_more": false,
  "url": "/api/developer/v1/sales"
}
Try it

Retrieve a sale

GET /api/developer/v1/sales/{id}

One sale, and whether it was cancelled or refunded since.

Scope: sales:read. Secret keys only. MCP tool: retrieve_sale.

Parameters

  • idstringrequiredin path

    The object’s id.

GET /sales/{id}
curl https://finpo.nikcano.com/api/developer/v1/sales/sale_id \
  -H "Authorization: Bearer fk_test_..."
Response
{
  "object": "sale",
  "id": "cmv2a1b2c0001sal",
  "receipt_number": "VIC-000018",
  "location": "cmv2a1b2c0001loc",
  "business_day": "2026-10-12",
  "created": "2026-10-12T13:05:00.000Z",
  "currency": "NGN",
  "total": 700000,
  "gross": 700000,
  "customer": null,
  "voided": false,
  "refunded": false,
  "lines": [],
  "payments": []
}
Try it

Orders

List orders

GET /api/developer/v1/orders

Orders the shop has received, from its own staff, its order page, its tables and the API, newest first.

Scope: orders:read. Secret keys only. MCP tool: list_orders.

Parameters

  • limitintegerin query

    How many to return, 1 to 100. The default is 20.

  • starting_afterstringin query

    The id of the last object of the page before: the next page starts after it.

  • ending_beforestringin query

    The id of the first object of the page after: the page before ends before it.

  • statusstringin query

    Only orders in this state.

    One of: newconfirmedpreparingreadyout_for_deliverycompletedcancelled

GET /orders
curl https://finpo.nikcano.com/api/developer/v1/orders?limit=20 \
  -H "Authorization: Bearer fk_test_..."
Response
{
  "object": "list",
  "data": [
    {
      "object": "order",
      "id": "cmv2a1b2c0001ord",
      "number": 42,
      "location": "cmv2a1b2c0001loc",
      "channel": "online",
      "fulfilment": "pickup",
      "status": "new",
      "customer": null,
      "customer_name": "Ngozi Eze",
      "customer_phone": "+2348031234567",
      "delivery_address": null,
      "due": null,
      "currency": "NGN",
      "total": 150000,
      "note": null,
      "lines": [
        {
          "product": "cmv2a1b2c0001prd",
          "unit": "FULL",
          "name": "Peak evaporated milk 160g",
          "quantity": 2,
          "unit_price": 75000,
          "amount": 150000
        }
      ],
      "sale": null,
      "created": "2026-10-12T09:00:00.000Z",
      "updated": "2026-10-12T09:00:00.000Z"
    }
  ],
  "has_more": false,
  "url": "/api/developer/v1/orders"
}
Try it

Retrieve an order

GET /api/developer/v1/orders/{id}

One order, with its lines and where it is now.

Scope: orders:read. Secret keys only. MCP tool: retrieve_order.

Parameters

  • idstringrequiredin path

    The object’s id.

GET /orders/{id}
curl https://finpo.nikcano.com/api/developer/v1/orders/order_id \
  -H "Authorization: Bearer fk_test_..."
Response
{
  "object": "order",
  "id": "cmv2a1b2c0001ord",
  "number": 42,
  "location": "cmv2a1b2c0001loc",
  "channel": "online",
  "fulfilment": "pickup",
  "status": "new",
  "customer": null,
  "customer_name": "Ngozi Eze",
  "customer_phone": "+2348031234567",
  "delivery_address": null,
  "due": null,
  "currency": "NGN",
  "total": 150000,
  "note": null,
  "lines": [
    {
      "product": "cmv2a1b2c0001prd",
      "unit": "FULL",
      "name": "Peak evaporated milk 160g",
      "quantity": 2,
      "unit_price": 75000,
      "amount": 150000
    }
  ],
  "sale": null,
  "created": "2026-10-12T09:00:00.000Z",
  "updated": "2026-10-12T09:00:00.000Z"
}
Try it

Create an order

POST /api/developer/v1/orders

Sends an order to the shop, the way a customer’s order from its order page arrives: staff see it on the Orders board, prepare it, and take the money at the counter, where it becomes a signed sale. Prices are always the shop’s own: the order is priced by finpo from the products and units sent. Safe with a publishable key, from a shop’s own website or app.

Scope: orders:write. Secret or publishable keys. MCP tool: create_order.

Parameters

  • customer_namestringrequired

    Who the order is for.

  • customer_phonestringrequired

    A number the shop can call.

  • fulfilmentstring

    Collected at the shop (the default) or delivered.

    One of: pickupdelivery

  • delivery_addressstring

    Where to deliver it. Needed for a delivery.

  • linesarray of objectrequired

    What is being ordered.

    • productstringrequired

      The product’s id.

    • unitstring

      The key of the unit it is sold in. The default is its first unit.

    • quantityintegerrequired

      How many.

  • notestring

    Anything the shop should know.

  • locationstring

    The location to send it to. The default is the main one.

POST /orders
curl -X POST https://finpo.nikcano.com/api/developer/v1/orders \
  -H "Authorization: Bearer fk_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"customer_name":"Ngozi Eze","customer_phone":"08031234567","lines":[{"product":"cmv2a1b2c0001prd","unit":"FULL","quantity":2}]}'
Response
{
  "object": "order",
  "id": "cmv2a1b2c0001ord",
  "number": 42,
  "location": "cmv2a1b2c0001loc",
  "channel": "online",
  "fulfilment": "pickup",
  "status": "new",
  "customer": null,
  "customer_name": "Ngozi Eze",
  "customer_phone": "+2348031234567",
  "delivery_address": null,
  "due": null,
  "currency": "NGN",
  "total": 150000,
  "note": null,
  "lines": [
    {
      "product": "cmv2a1b2c0001prd",
      "unit": "FULL",
      "name": "Peak evaporated milk 160g",
      "quantity": 2,
      "unit_price": 75000,
      "amount": 150000
    }
  ],
  "sale": null,
  "created": "2026-10-12T09:00:00.000Z",
  "updated": "2026-10-12T09:00:00.000Z"
}

Webhook endpoints

List webhook endpoints

GET /api/developer/v1/webhook_endpoints

The addresses finpo sends events to. The signing secret is shown only when an endpoint is created.

Scope: webhooks:write. Secret keys only. MCP tool: list_webhook_endpoints.

No parameters.

GET /webhook_endpoints
curl https://finpo.nikcano.com/api/developer/v1/webhook_endpoints \
  -H "Authorization: Bearer fk_test_..."
Response
{
  "object": "list",
  "data": [
    {
      "object": "webhook_endpoint",
      "id": "cmv2a1b2c0001whk",
      "url": "https://example.com/finpo/webhooks",
      "enabled_events": [
        "sale.recorded",
        "order.created"
      ],
      "active": true,
      "created": "2026-10-12T09:00:00.000Z"
    }
  ],
  "has_more": false,
  "url": "/api/developer/v1/webhook_endpoints"
}
Try it

Create a webhook endpoint

POST /api/developer/v1/webhook_endpoints

Starts sending the chosen events to an address. The answer carries the signing secret, once: keep it, and check every delivery’s `Finpo-Signature` header with it (see Webhooks).

Scope: webhooks:write. Secret keys only. MCP tool: create_webhook_endpoint.

Parameters

  • urlstringrequired

    An https address of yours.

  • enabled_eventsarray of stringrequired

    Which events to send: sale.recorded, stock.changed, item.upserted, customer.upserted, order.created.

POST /webhook_endpoints
curl -X POST https://finpo.nikcano.com/api/developer/v1/webhook_endpoints \
  -H "Authorization: Bearer fk_test_..." \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/finpo/webhooks","enabled_events":["sale.recorded","order.created"]}'
Response
{
  "object": "webhook_endpoint",
  "id": "cmv2a1b2c0001whk",
  "url": "https://example.com/finpo/webhooks",
  "enabled_events": [
    "sale.recorded"
  ],
  "active": true,
  "created": "2026-10-12T09:00:00.000Z",
  "secret": "whsec_..."
}

Delete a webhook endpoint

DELETE /api/developer/v1/webhook_endpoints/{id}

Stops sending events to that address.

Scope: webhooks:write. Secret keys only. MCP tool: delete_webhook_endpoint.

Parameters

  • idstringrequiredin path

    The object’s id.

DELETE /webhook_endpoints/{id}
curl -X DELETE https://finpo.nikcano.com/api/developer/v1/webhook_endpoints/webhook_id \
  -H "Authorization: Bearer fk_test_..."
Response
{
  "object": "webhook_endpoint",
  "id": "cmv2a1b2c0001whk",
  "deleted": true
}

Changelog

2026-10-12: the first version. Products, stock levels, customers, sales, orders, locations, categories, webhook endpoints; secret and publishable keys; test mode; the MCP server.

API reference | finpo