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[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 });
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
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
Returns every book linked to your app, most recent first.
Get a book
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.
Verify payment
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
Once an order is paid, exchanges it for a signed file URL, valid for 2 minutes.
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.
{ "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 |
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 |