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 });
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.
| Permission | Where it is given | What it opens |
|---|---|---|
| Library access | Dashboard › Books, per app | Only 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 scopes | Dashboard › API keys, per key | What one key may do with the books its app can reach: read, write, fetch files, distribute. |
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
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
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
Returns one book in the same shape as the list.
Get the book file
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
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
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
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.
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 in | Ways to pay |
|---|---|
| Nigeria | Card, bank transfer, USSD, pay with bank (OPay and other bank apps), mobile money, QR |
| Any other country | Card 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
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
Once an order is paid, exchanges it for a signed file URL, valid for 2 minutes.
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.
GET /v1/app returns the current figure as sale_fee_percent.| The book belongs to | Who is paid | Where it shows |
|---|---|---|
| You, the account owner, with a Paystack settlement account that Paystack has verified | 80% 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 once | Your 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 API | API dashboard › Authors, and GET /v1/authors/:id/wallet |
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.
| Page | What it is | Set-up |
|---|---|---|
| store.html | A 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.html | Paste 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. |
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.
{ "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 |
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
Registers a storefront your app operates — a domain, a school library, a church store — as a distribution target.
Push a book to marketplaces
{
"book_id": "bk_8937",
"marketplace_ids": ["mk_201", "mk_309"]
}
Each target syncs independently — a failure on one marketplace never blocks the others.
Webhooks
{ "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 |