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 dashboardApps → 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[email protected] \
  -F cover[email protected]
// → { "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..."

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 books, create marketplaces, register webhooks
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 integer minor units
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 every book linked to your app, most recent first.

Get a book

GET/v1/books/:idbooks:read

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.

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's wallet. Call this on whatever page the buyer lands on after paying — never trust the client-side payment callback alone.

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.

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' });

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[email protected]
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