API & webhook documentation
Create flipbooks automatically from your own system with the anflip API, and get webhooks when a flipbook is ready, a conversion fails, or a new lead arrives.
Last updated September 29, 2026
The anflip API lets your own system (a CMS, an online store, an internal app) create and manage flipbooks without uploading through the dashboard. Webhooks go the other way: anflip tells your system when something happens.
The API is available on every plan. File size, page count, number of flipbooks, and storage follow the plan of the team that owns the API key.
When to use it
- Catalogs that change often. Your store generates a price catalog PDF every week and sends it to anflip through the API. The flipbook link goes on your website automatically.
- Regular publications. Magazines, newsletters, or monthly reports become flipbooks as soon as the final PDF is ready. The
flipbook.readywebhook tells your system to publish the link. - Leads straight to sales. The
lead.createdwebhook sends what readers type in a lead form to your CRM, WhatsApp, or Telegram within seconds. - Many clients. Agencies create and manage flipbooks for dozens of clients from their internal tools.
Quick start
- Open Dashboard → API, name the key (for example "Online store"), and click Create key. Copy the key: it is shown only once.
- Upload your first PDF:
curl -X POST https://manage.anflip.id/api/v1/ext/flipbooks \
-H "Authorization: Bearer afl_xxxxxxxxxxxxxxxx" \
-H "Accept: application/json" \
-F "file=@catalog.pdf" \
-F "title=October catalog" \
-F "visibility=unlisted"
- Wait for the conversion with the
flipbook.readywebhook or by checkingGET /flipbooks/{id}, then share thepublic_url.
Authentication & general rules
Send the API key with every request:
Authorization: Bearer afl_xxxxxxxxxxxxxxxx
Accept: application/json
| Item | Value |
|---|---|
| Base URL | https://manage.anflip.id/api/v1/ext |
| Format | JSON, except file uploads (multipart/form-data) |
| Rate limit | 60 requests per minute per key. Above that you get 429 |
| Error language | Indonesian. Add Accept-Language: en for English |
| Keys | Up to 10 per team |
| Owner | Flipbooks are created in the key's team, on behalf of the member who created the key |
.env file or a secret manager). Never put it in code that runs in a browser or a mobile app, where anyone can read it. If a key leaks, delete it in Dashboard → API and create a new one.The flipbook object
Every endpoint answers with flipbooks in this shape (inside data):
{
"id": "01J9Z6K3Q4T8V2N5M7P0R1S3W4",
"title": "October catalog",
"slug": "5f2a9c01be",
"status": "ready",
"visibility": "unlisted",
"page_count": 24,
"progress": 100,
"converting": false,
"has_live_version": true,
"has_password": false,
"allow_download": false,
"error": null,
"cover_url": "https://cdn.anflip.id/fb/01J9…/v1/p1-md.webp",
"public_url": "https://anflip.id/flip-book/5f2a9c01be",
"storage_bytes": 5241880,
"created_at": "2026-10-01T02:00:00+00:00",
"processed_at": "2026-10-01T02:00:41+00:00",
"published_at": "2026-10-01T02:00:00+00:00"
}
| Field | Meaning |
|---|---|
status | queued, processing, ready, or failed |
progress | Conversion progress, 0–100 |
has_live_version | true once there are pages to read. Use this, not status, to know a flipbook can be shared |
converting | true while a conversion runs (also while a PDF is being replaced) |
visibility | private (team only), unlisted (anyone with the link, not indexed by Google), public |
public_url | The link for readers |
error | Why it failed when status is failed, for example a damaged or password-protected PDF |
Endpoints
Create a flipbook
POST /flipbooks as multipart/form-data. The conversion runs in the background, so the answer comes right away with status 202 and status: "queued".
| Field | Required | Notes |
|---|---|---|
file | yes | A PDF. The maximum size follows the team's plan |
title | no | Title. Defaults to the file name |
visibility | no | private (default), unlisted, or public. For unlisted and public the key's creator must have verified their email |
List flipbooks
GET /flipbooks returns the 50 newest flipbooks per page. Use ?page=2 for the next page. Paging details are in meta (current_page, last_page, total).
Get a flipbook
GET /flipbooks/{id} returns one flipbook. Handy for checking a conversion.
Delete a flipbook
DELETE /flipbooks/{id}. The flipbook's link stops working right away. Answer: { "data": { "id": "…", "deleted": true } }.
Waiting for the conversion
A 20–50 page PDF usually takes under a minute; hundreds of pages can take a few minutes. Two ways to wait:
- The
flipbook.readywebhook (recommended). No repeated checks: anflip tells you when it is done. - Polling. Check
GET /flipbooks/{id}every 5–10 seconds untilhas_live_versionistrueorstatusisfailed.
async function waitUntilReady(id) {
for (;;) {
const res = await fetch(`https://manage.anflip.id/api/v1/ext/flipbooks/${id}`, {
headers: { Authorization: `Bearer ${process.env.ANFLIP_KEY}`, Accept: "application/json" },
});
const { data } = await res.json();
if (data.has_live_version) return data.public_url;
if (data.status === "failed") throw new Error(data.error);
await new Promise((r) => setTimeout(r, 8000));
}
}
Errors
| Status | Meaning | What to do |
|---|---|---|
401 | Wrong, deleted, or missing API key | Check the Authorization header |
403 | Not allowed, for example publishing when the key's creator has not verified their email | Read message |
404 | The flipbook does not exist or belongs to another team | Check the id |
422 | Invalid input: not a PDF, file too large, or the plan's quota is full | Per-field details are in errors |
429 | More than 60 requests per minute | Wait a moment and retry |
5xx | A problem on anflip's side | Retry a little later |
Example 422 answer:
{
"message": "Your plan allows 5 flipbooks. Delete old ones or upgrade your plan.",
"errors": { "file": ["Your plan allows 5 flipbooks. Delete old ones or upgrade your plan."] }
}
Webhooks
Adding a webhook
In Dashboard → API → Webhook:
- Enter a URL on your system. It must be
https://and reachable from the internet (notlocalhostor a private network address). - Pick the events you want, then click Add webhook.
- Copy the signing secret (starting with
whsec_). It is shown only once and is used to verify deliveries. - Send a ping to try it. The last delivery status shows in the webhook list.
Up to 5 webhooks per team.
Events
| Event | Sent when | data contains |
|---|---|---|
flipbook.ready | A conversion finishes, also after a PDF is replaced | The full flipbook object |
flipbook.failed | A conversion fails | id, title, error (always in English) |
lead.created | A reader fills in a flipbook's lead form | id, flipbook (id, title, url), data (the answers), created_at |
ping | You press the ping button in the dashboard | team |
lead.created answers only hold the fields the form uses: name, email, phone, company, and message.
Delivery format
anflip sends a POST with a JSON body like this:
{
"id": "evt_01J9Z7A2B3C4D5E6F7G8H9J0K1",
"event": "lead.created",
"created_at": "2026-10-01T03:30:00+00:00",
"data": {
"id": 128,
"flipbook": { "id": "01J9Z6K3Q4T8V2N5M7P0R1S3W4", "title": "October catalog", "url": "https://anflip.id/flip-book/5f2a9c01be" },
"data": { "name": "Sari", "email": "sari@example.com", "phone": "081234567890" },
"created_at": "2026-10-01T03:30:00+00:00"
}
}
Headers sent along:
| Header | Content |
|---|---|
Content-Type | application/json |
X-Anflip-Event | The event name, for example lead.created |
X-Anflip-Signature | sha256= followed by the HMAC-SHA256 of the raw body, using the signing secret |
User-Agent | anflip-webhooks/1.0 |
Verifying the signature
Always check the signature before processing, so forged deliveries are rejected. Compute the HMAC over the raw body (exactly as received, before parsing it as JSON).
// Node.js (Express): app.post("/anflip-webhook", express.raw({ type: "application/json" }), handler)
import crypto from "node:crypto";
function verify(rawBody, header, secret) {
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(header ?? "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// PHP / Laravel
$expected = 'sha256='.hash_hmac('sha256', $request->getContent(), env('ANFLIP_WEBHOOK_SECRET'));
abort_unless(hash_equals($expected, (string) $request->header('X-Anflip-Signature')), 401);
# Python (Flask)
import hashlib, hmac
def verify(raw_body: bytes, header: str, secret: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, header or "")
Answering, retries, and good practice
- Answer
2xxwithin 10 seconds. Store the delivery and process it in the background (a queue). Don't make anflip wait for long work. - Failed deliveries are retried (non-
2xx, timeout, or no connection) up to 5 times in total, after 10 seconds, 1 minute, 5 minutes, and 30 minutes. - Expect duplicates. Because of retries the same event can arrive more than once. Store the event
id(evt_…) and skip ones you already processed. - Order is not guaranteed. Use
created_atwhen order matters. - Redirects are not followed. Register the final URL, not one that redirects.
Full example: a weekly catalog
A store creates catalog.pdf every Monday, sends it to anflip, and puts the link on its website once it is ready.
// Laravel: run by the scheduler every Monday
$response = Http::withToken(env('ANFLIP_KEY'))->acceptJson()
->attach('file', file_get_contents(storage_path('catalog.pdf')), 'catalog.pdf')
->post('https://manage.anflip.id/api/v1/ext/flipbooks', [
'title' => 'Catalog '.now()->format('F j, Y'),
'visibility' => 'unlisted',
])->throw();
Setting::put('catalog_pending', $response->json('data.id'));
// Webhook route: POST /anflip-webhook
$expected = 'sha256='.hash_hmac('sha256', $request->getContent(), env('ANFLIP_WEBHOOK_SECRET'));
abort_unless(hash_equals($expected, (string) $request->header('X-Anflip-Signature')), 401);
if ($request->input('event') === 'flipbook.ready'
&& $request->input('data.id') === Setting::get('catalog_pending')) {
Setting::put('catalog_url', $request->input('data.public_url')); // shown on the website
}
return response()->noContent();
No code
- Google Sheets and Mailchimp connect directly in Dashboard → Integrations, without the API.
- Other apps (CRM, Slack, email, WhatsApp gateways): point a webhook at Zapier, Make, or n8n with a "Catch webhook" / "Webhooks" trigger, then pass it on.