Quick start

For a new organization, start to finish: get keys, upload a book, list it, take a payment. About ten minutes.

1. Create an app and get your keys

Log in to your dashboard → Apps → New app. Then open the app → API keys → Generate key. You'll get two keys — save the secret one now, it's shown once:

Key Used for Where it lives
sk_live_... Uploading books, anything with books:write Your server only — never the browser
pk_live_... Listing books, checkout, the SDK Safe in browser/frontend code

2. Upload a book (server-side, sk_live)

One request creates the book, stores the PDF and cover on R2, and generates the slug — nothing about the book's identity is something you set yourself.

curl https://api-afrikdp.onrender.com/v1/books \
  -H "Authorization: Bearer sk_live_..." \
  -F title="Ubuntu" \
  -F description="..." \
  -F price=2500 \
  -F file=@ubuntu.pdf \
  -F cover=@cover.jpg
// → { "book_id": "bk_8937", "slug": "ubuntu-8937a1c2", "status": "review" }

Save the book_id — that's what you'll list, fetch, and check out against everywhere else.

3. List or fetch that book (frontend, pk_live)

Once approved, read access only needs the public key — safe to call straight from a browser.

fetch('https://api-afrikdp.onrender.com/v1/books/bk_8937', {
  headers: { 'x-api-key': 'pk_live_...' }
}).then(r => r.json());

4. Take a payment — the easy way (SDK)

This is the whole integration for a working "Buy" button. No fetch calls, no checkout page to build:

<script src="https://api-afrikdp.onrender.com/afrikdp-sdk.js"
        data-public-key="pk_live_..."></script>

<button onclick="AfriKDP.buyBook('bk_8937')">Buy this book</button>

That opens AfriKDP's branded checkout modal — showing your app's name, not just "AfriKDP" — collects the email, hands off to Paystack's secured popup for the card entry, and lands the buyer back on your page. On that landing page, one call confirms the sale and gets the file:

AfriKDP.handleReturn()
  .then(result => AfriKDP.getDownloadLink(result.order_id))
  .then(({ download_url }) => {
    // show a "Download now" link pointing at download_url
    // it expires 2 minutes after you request it
  });
See the full SDK reference — every function, and the raw-API equivalents if you want to build your own checkout UI instead of the built-in modal — in the SDK section below.

Authentication

Every request is made with an API key generated from your dashboard. Two kinds:

Key Header Access
sk_live_... Authorization: Bearer Full access, scoped by permission
pk_live_... x-api-key Read-only, safe to expose in a browser

Use pk_live keys in client-side storefront code. Keep sk_live keys on your server — never in a mobile app or browser bundle.

curl https://api-afrikdp.onrender.com/v1/books \
  -H "Authorization: Bearer sk_live_9fJH82k..."

Permissions

Two separate permissions decide what an integration can touch. Both are given by the account owner in the dashboard, both can be taken back at any time.

PermissionWhere it is givenWhat it opens
Library accessDashboard › Books, per appOnly this app's books (default) or every book on the AfriKDP account. Opening the whole library shows a permission screen that the account owner must accept. Only the owner of the organization can accept it.
Key scopesDashboard › API keys, per keyWhat one key may do with the books its app can reach: read, write, fetch files, distribute.
Accounts are shared with afrikdp.com. Signing up or signing in here uses the same AfriKDP account, profile and wallet, and a book uploaded through the API belongs to that account like any other.

Scopes

Every secret key carries a permission set decided when it's generated. Request only what an integration needs.

Scope Grants
books:read List and fetch books
books:write Upload and edit books, create marketplaces, register webhooks
books:file Fetch a private link to a book's PDF or ZIP. Off unless you tick it when the key is made
distribution Push a book to partner marketplaces

Errors & rate limits

Errors return a JSON body with an error string and the matching HTTP status — 401 for a bad key, 403 for a missing scope, 422 for a bad request, 429 once you're over your rate limit.

Every app starts at 100 requests/minute and 20,000/day. Usage and current limits are visible from your dashboard.

Books

POST/v1/booksbooks:write

Multipart upload. Stores the file and cover on Cloudflare R2 and creates the book in review status.

Field Type
title string required
description string required
file file required, PDF
cover file optional, image
price number US dollars, e.g. 4.99. 0 or empty = free. A paid book needs a verified identity on AfriKDP.
language string optional, default English
tags string optional, comma separated
external_book_id string your own ID, for mapping back
visibility string private · public · unlisted
{
  "book_id": "bk_8937",
  "partner_link_id": "pl_1029",
  "status": "review"
}

List books

GET/v1/booksbooks:read

Returns the books your app can reach, most recent first. Which books those are is set per app in the dashboard under Books: every book on your AfriKDP account (the ones you uploaded on afrikdp.com and the ones uploaded through the API), or only books uploaded through this app.

Query: page, limit (up to 100), q (search), category, status (secret key only). A public key only ever receives books that are live.

{
  "books": [{
    "id": "6f1c…", "title": "My Book", "slug": "my-book-6f1c2a9e",
    "description": "…", "category": "Business", "language": "English", "tags": [],
    "price": 4.99, "currency": "USD", "price_ngn": 6636,
    "cover_url": "https://…/cover.webp", "status": "live", "source": "afrikdp",
    "page_count": 120, "buyers": 34, "rating": 4.8, "rating_count": 12,
    "url": "https://afrikdp.com/book/my-book-6f1c2a9e"
  }],
  "page": 1, "limit": 50, "total": 1, "scope": "account"
}

Get a book

GET/v1/books/:idbooks:read

Returns one book in the same shape as the list.

Get the book file

GET/v1/books/:id/filebooks:read + books:file · secret key

Returns a private link to the PDF (or ZIP) that works for 5 minutes (?expires_in=60 to 900 seconds). Call it from your server with your secret key, then hand the link to the reader who is entitled to the book. It never works with a public key, so the file cannot be fetched from a browser.

{
  "book_id": "6f1c…", "file_type": "pdf", "file_size": 1843200,
  "download_url": "https://…signed…", "expires_in_seconds": 300
}

Update a book

PATCH/v1/books/:idbooks:write

Send only what changes: title, description, category, language, tags, price (US dollars), a new file, a new cover. New words, a new file or a new cover send the book back through review, exactly like an edit on afrikdp.com. A price change alone does not.

App info

GET/v1/appbooks:read

The name buyers see in the checkout, and the public key that belongs to the same key pair. A public key is not a secret: a tool that only holds your secret key can use this to open the browser checkout correctly.

{
  "name": "My Bookshop", "display_name": "My Bookshop",
  "public_key": "pk_live_…", "environment": "live", "sale_fee_percent": 20
}

Checkout

POST/v1/checkoutbooks:read

Creates an order. Books are priced and shown in USD everywhere on this platform, but the Paystack account behind this API only accepts NGN — every non-free order is converted at checkout time, and this endpoint returns the full breakdown so you can show the buyer exactly what they're paying before any conversion happens, not after.

{ "book_id": "bk_8937", "buyer_email": "[email protected]", "buyer_country": "NG" }
// →
{
  "order_id": "ord_552",
  "reference": "afk_9fJH82k...",
  "amount": 1075000,
  "currency": "NGN",
  "subtotal_usd": 10.00,
  "vat_usd": 0.75,
  "vat_percent": 7.5,
  "total_usd": 10.75,
  "fx_rate": 1000,
  "amount_ngn": 10750,
  "paystack_public_key": "pk_live_...",
  "free": false
}

amount is what actually gets sent to Paystack's popup — NGN, in kobo. Everything else in the response is for display: subtotal_usd/vat_usd/total_usd are the real USD breakdown to show the buyer, fx_rate and amount_ngn are the converted total so they see the NGN amount coming before confirming, not as a surprise on the card screen.

A price of 0 skips all of this — no VAT, no conversion, no Paystack. The order is marked paid immediately and the response is just { "order_id": "...", "free": true }.

VAT_PERCENT and USD_TO_NGN_RATE are both set via environment variables on the backend, not hardcoded — USD_TO_NGN_RATE has no default and deliberately refuses to process a paid checkout if it isn't configured, rather than silently charging the wrong amount.
Most integrations never call this directly — AfriKDP.buyBook() in the SDK does this, shows the buyer this exact breakdown with an explicit confirm step, then opens Paystack's popup. Call it yourself only if you're building a fully custom checkout UI.

Ways to pay, by country

The checkout offers the methods that actually work where the buyer is. Send buyer_country (two letters, e.g. NG) with POST /v1/checkout; if you leave it out, AfriKDP uses the country the buyer is connecting from. The built-in checkout window picks the country for the buyer automatically and lets them change it.

Buyer is inWays to pay
NigeriaCard, bank transfer, USSD, pay with bank (OPay and other bank apps), mobile money, QR
Any other countryCard only. The Nigerian local methods cannot be completed from abroad

The answer to POST /v1/checkout tells you what was offered: buyer_country, payment_channels and payment_methods. The checkout answer also carries settlement: split_to_author_bank or afrikdp_wallet. GET /v1/geo (no key needed) returns the visitor's country and the methods they will see.

Verify payment

POST/v1/checkout/verifybooks:read

Confirms the transaction with Paystack and marks the order paid. Fires a sale.completed webhook and credits the seller. Call this on whatever page the buyer lands on after paying — never trust the client-side payment callback alone.

You do not have to call it for the author to be paid. AfriKDP is told by Paystack the moment a payment lands and settles the order itself, so a buyer who closes the tab too early still produces a paid order and a credited author. Calling verify twice is safe: an order is only ever credited once.

Download token

POST/v1/download-tokenbooks:read

Once an order is paid, exchanges it for a signed file URL, valid for 2 minutes.

The signed URL is single-purpose and expires quickly on purpose — request it right before the reader clicks download, not ahead of time.

Where the money goes

One AfriKDP account has one wallet. A sale made through your own website lands in the same wallet as a sale on afrikdp.com.

The fee on API sales is 20%. For every sale made through the API, AfriKDP keeps 20% and the author receives 80%. (Sales made on afrikdp.com itself keep their own 15% fee.) GET /v1/app returns the current figure as sale_fee_percent.
The book belongs toWho is paidWhere it shows
You, the account owner, with a Paystack settlement account that Paystack has verified80% is sent by Paystack straight to your own bank account (next working day). Only AfriKDP's 20% comes to AfriKDP. Nothing is added to the wallet for that sale.Your AfriKDP author dashboard counts the sale at once; the wallet page lists it under “Waiting for Paystack”, then “Paid to your bank”.
You, the account owner, without a verified settlement account (or on fast release, or routed to another gateway by AfriKDP)80% to your AfriKDP wallet, at onceYour AfriKDP author dashboard: earnings, sales count, monthly chart, wallet balance. You withdraw it the normal way.
One of your organization's authors
(author_id was set on the book)
80% to that author's own wallet in this APIAPI dashboard › Authors, and GET /v1/authors/:id/wallet
Free books and pay-what-you-want books are priced correctly by the API: a free book is 0 whatever old number is stored, and a pay-what-you-want book costs at least its minimum.

Client SDK

A ~14KB vanilla-JS file that wraps this whole API, plus the checkout modal shown in the quick start above. No dependencies, no build step.

<script src="https://api-afrikdp.onrender.com/afrikdp-sdk.js"
        data-public-key="pk_live_..."></script>
Function Returns
AfriKDP.getApp() { name, display_name }
AfriKDP.getBook(id) book
AfriKDP.listBooks() book[]
AfriKDP.buyBook(id, opts) — opens the checkout modal
AfriKDP.checkout({bookId, email}) order + Paystack inline data
AfriKDP.verify(reference) { status, order_id }
AfriKDP.getDownloadLink(orderId) { download_url }
AfriKDP.handleReturn() verifies whatever's in the current URL
AfriKDP.checkHealth() { status, latency_ms }

buyBook(id, opts) takes an optional { successUrl } — where the buyer lands after paying. Defaults to the current page.

AfriKDP.buyBook('bk_8937', { successUrl: 'https://yoursite.com/thank-you.html' });

Ready-made pages

Two single-file pages you can use as they are. Both keep the buyer on your own site: a book opens in a details sheet, and payment happens in AfriKDP's checkout on top of the page. Nothing sends the buyer to afrikdp.com.

PageWhat it isSet-up
store.htmlA complete bookstore: search, categories, sorting, dollar / naira switch, book details, Buy now or Get it free, payment confirmation and download.Paste your public key into STORE.publicKey at the top of the file and upload it to your website.
api-tester.htmlPaste any key and see what it reaches and what it may do: seven pass / fail checks, the books with covers, every field of a book, the raw answers, and a real test purchase.Nothing to set up. The key is typed in each time and lives only in that browser tab.
Never write a secret key (sk_live_) into a web page. Anyone can read it with “view source”. Web pages use the public key; the secret key stays on your server.

The shortest possible integration

<script src="https://api-afrikdp.onrender.com/afrikdp-sdk.js"
        data-public-key="pk_live_…"></script>

<button onclick="AfriKDP.buyBook('BOOK_ID', { successUrl: location.href })">Buy this book</button>

<script>
  // on the page the buyer comes back to
  if (location.search.includes('reference') || location.search.includes('order_id')) {
    AfriKDP.handleReturn()
      .then(r => AfriKDP.getDownloadLink(r.order_id))
      .then(d => { location.href = d.download_url; });   // the link lasts two minutes: ask for it when the buyer clicks
  }
</script>

Author sub-accounts

For a platform with its own authors — a publisher, a school, a marketplace with many creators — each author can get their own AfriKDP wallet. A book uploaded with an author_id pays that author directly; the sale never touches your organization's own wallet. Leave author_id out entirely for a solo/personal account — nothing changes for that case.

POST/v1/authorsbooks:write
{ "name": "Florence Cook", "email": "[email protected]" }
// → { "author": { "id": "...", "name": "Florence Cook", "status": "active" } }

Attribute a book to an author

Pass the author's id when uploading (or updating) a book:

curl https://api-afrikdp.onrender.com/v1/books \
  -H "Authorization: Bearer sk_live_..." \
  -F title="Embracing Sankofa" \
  -F price=2500 \
  -F author_id="the-author-id-above" \
  -F file=@sankofa.pdf
Endpoint Scope Returns
GET /v1/authors books:read every author under your org
GET /v1/authors/:id/wallet books:read that author's balance
GET /v1/authors/:id/ledger books:read recent wallet activity
POST /v1/authors/:id/withdrawals books:write requests a payout for that author
GET /v1/authors/:id/withdrawals books:read that author's withdrawal history
The afrikdp Node.js and Python packages wrap all of this — client.createAuthor(), client.uploadBook({..., authorId}), client.getAuthorWallet(), and so on. No need to hand-roll these requests.

Distribution

POST/v1/marketplacesbooks:write

Registers a storefront your app operates — a domain, a school library, a church store — as a distribution target.

Push a book to marketplaces

POST/v1/distributiondistribution
{
  "book_id": "bk_8937",
  "marketplace_ids": ["mk_201", "mk_309"]
}

Each target syncs independently — a failure on one marketplace never blocks the others.

Webhooks

POST/v1/webhooksbooks:write
{ "url": "https://yoursite.com/hooks/afrikdp", "events": ["book.created", "sale.completed"] }

Every delivery is signed. Verify it with the secret returned once at creation time:

const signature = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
// compare against the X-AfriKDP-Signature header

Event types

Event Fires when
book.created A book finishes uploading
sale.completed A checkout is verified paid