Implementing Electronic Book Sales (PDF/EPUB) on a Website
Digital book sales are simpler than courses, but still require nontrivial solutions: file protection from free distribution, fast delivery after payment, optional DRM, and correct refund handling.
File Delivery Schema
Main rule: files must not be in publicly accessible directory. No /public/books/my-book.pdf. Storage — S3-compatible object storage (AWS S3, Cloudflare R2, MinIO) without public ACL.
After payment confirmation, user receives temporary signed link:
// Laravel + AWS S3
$url = Storage::disk('s3')->temporaryUrl(
"books/{$book->file_key}",
now()->addHours(48),
['ResponseContentDisposition' => 'attachment; filename="' . $book->filename . '"']
);
Link lives 48 hours. Redownload via personal account — each time generates new link. Download count can be limited: table download_attempts(purchase_id, downloaded_at), limit — e.g., 5 downloads per purchase.
Payment Integration
For digital goods, Stripe recommends payment_intent or checkout.session. Key moment — tax flag: digital books in some jurisdictions have different VAT than physical goods. Stripe Tax can auto-detect by payer IP/address.
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [{
price: book.stripe_price_id, // pre-created Price in Stripe Dashboard
quantity: 1,
}],
automatic_tax: { enabled: true },
metadata: { book_id: book.id, user_id: user.id },
success_url: `${APP_URL}/library?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${APP_URL}/books/${book.slug}`,
});
Webhook checkout.session.completed creates purchases record and sends email with download link.
Data Structure
CREATE TABLE purchases (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id BIGINT REFERENCES users(id),
book_id BIGINT REFERENCES books(id),
payment_id VARCHAR(255) UNIQUE,
amount_cents INT,
currency VARCHAR(3),
status VARCHAR(20) DEFAULT 'completed', -- completed | refunded
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE download_attempts (
id BIGSERIAL PRIMARY KEY,
purchase_id UUID REFERENCES purchases(id),
ip_address VARCHAR(45),
user_agent TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
Formats: PDF vs EPUB
Most customers prefer PDF for desktop reading and EPUB for mobile readers (Kindle, Apple Books, Kobo). Selling both formats in one purchase is good practice.
Store files with different suffixes in one bucket:
books/
{uuid}-original.epub
{uuid}-print.pdf
{uuid}-cover.jpg
Table book_files(book_id, format, file_key, file_size_bytes).
Optional Watermark
For expensive books, add personalized watermark with buyer's email. Not DRM, but psychologically restrains distribution.
PDF watermark via pypdf (Python) or iTextSharp (.NET). In PHP ecosystem — setasign/fpdi:
use setasign\Fpdi\Fpdi;
$pdf = new Fpdi();
$pageCount = $pdf->setSourceFile($sourcePath);
for ($i = 1; $i <= $pageCount; $i++) {
$pdf->AddPage();
$pdf->useTemplate($pdf->importPage($i));
$pdf->SetFont('Helvetica', '', 8);
$pdf->SetTextColor(180, 180, 180);
$pdf->SetXY(10, 285);
$pdf->Write(0, "Licensed to: {$purchase->user->email}");
}
$pdf->Output($outputPath, 'F');
Generation happens async in queue (Laravel Jobs / Bull / Celery), then link updates. For books under 10 MB takes 2–5 seconds.
Email Delivery
After purchase, email with download button should arrive within 30–60 seconds. Don't do sync in HTTP request — send via queue:
// In webhook handler
ProcessPurchase::dispatch($purchase)->onQueue('purchases');
// In Job
class ProcessPurchase implements ShouldQueue {
public function handle() {
// 1. Generate watermark (optional)
// 2. Create signed URL
// 3. Send email
Mail::to($this->purchase->user)->send(
new BookPurchasedMail($this->purchase, $downloadUrl)
);
}
}
Personal Library
User should redownload book via /library. List all purchases with "Download" button, calls endpoint generating new temporary URL. Important — storing permanent link in DB is useless, it expires.
Timelines
| Task | Time |
|---|---|
| File upload, S3, protection | 1 day |
| Payment integration + webhook | 1–2 days |
| Personal library, redownload | 1 day |
| Email delivery | 0.5 day |
| Watermark (PDF) | 1–2 days |
Basic implementation without watermark — 3–4 days.







