
How to Build a Comedogenic Ingredient Checker with a REST API
Table of Contents
- Why Static Comedogenic Lists Break Down in Production Apps
- The Data Model: Fields a Comedogenic Ingredient API Must Return
- Choosing Between GET Single-Ingredient and POST Batch Endpoints
- Wiring the Request-Response Loop: From INCI Input to Scored Output
- Translating 0–5 Scores into User-Facing Labels Without Lying
- Three Production Architecture Patterns for Shipping the Checker
- Pitfalls That Break Comedogenic Checkers in Production
- Pre-Ship Checklist for a Production Comedogenic Ingredient Checker
- FAQ: Implementation Questions Developers Actually Ask
You've shipped the scanner. The user points their phone at a moisturizer's INCI list, the OCR works, the ingredients populate — and then your app stalls on the question every acne-prone user actually came for: will this clog my pores?
That question sounds simple. It is not. The EU's CosIng database alone lists over 30,000 cosmetic ingredients, and acne affects roughly 9.4% of the global population — the eighth most prevalent disease worldwide according to Global Burden of Disease analyses. No spreadsheet survives that scale. No hand-curated JSON file shipped with your v1.0 stays current past the next regulatory cycle. And no single source-of-truth exists for what "comedogenic" actually means at the integer level your app's UI demands. This guide walks through the architecture, data model, and trade-offs for building a production-grade comedogenic ingredient checker against a REST API — the fields it must expose, the endpoints that fit which UX, the request-response loop in implementation detail, and the pitfalls that quietly break trust once you ship.

Why Static Comedogenic Lists Break Down in Production Apps
The 0–5 comedogenicity scale most apps rely on traces back to Fulton JE Jr.'s 1989 study in the Journal of the Society of Cosmetic Chemists, which tested 30+ ingredients on rabbit ear models — a methodology inherited from Kligman & Mills' 1972 "Acne cosmetica" work in Archives of Dermatology. That work was important. It is also more than three decades old, and its assumptions are now widely critiqued by the dermatology community whose lists derive from it.
The first technical problem is methodological: rabbit ear assays are more sensitive than human facial skin and produce false positives. According to Draelos ZD, Cosmetics and dermatologic problems and solutions 3rd ed. (CRC Press, 2012), ingredients rated 4–5 on rabbit ears — including isopropyl myristate and certain coconut derivatives — often behave benignly in well-formulated modern products. Any static list your app ships with is inheriting those decades-old false positives without flagging them as such.
The second problem is conflicting ratings across sources. Isopropyl myristate is rated 5 in Fulton's original data but lower in modern dermatology references depending on vehicle and concentration. If your developer team copy-pastes scores from one source while a competitor copy-pastes from another, users comparing both apps on the same product see different verdicts. Your credibility evaporates the moment they screenshot the discrepancy.
The third problem is regulatory drift. The EU Cosmetic Products Regulation (EC) No 1223/2009 bans over 1,600 substances (Annex II) and restricts hundreds more with conditional concentration limits (Annex III). The EU's Scientific Committee on Consumer Safety (SCCS) issues opinions multiple times per year, feeding into CosIng updates. A static JSON blob shipped with your v1.0 is stale within a quarter.
The fourth problem is jurisdictional: the FDA does not define "non-comedogenic". Per the FDA's own "Hypoallergenic Cosmetics" guidance and its "FDA Authority Over Cosmetics" page, U.S. cosmetics don't require pre-market approval (color additives excepted). Your app cannot defer to a U.S. regulatory body for the comedogenic label — there is none. EU regulation is centralized and pre-market; U.S. is post-market. Your data model must encode this asymmetry through a region field rather than assuming a global default.
The fifth problem is that dose-response is ignored by static lists. James E. Fulton Jr. himself wrote that "the comedogenicity numbers should be used as a guide rather than an absolute. The same ingredient may be highly comedogenic in certain formulations yet harmless when properly diluted or combined with non-comedogenic vehicles" (Fulton, 1989). Zoe Diana Draelos reinforces this in Cosmetic Dermatology: Products and Procedures, 2nd ed. (Wiley, 2015): "The comedogenicity of an ingredient cannot be considered in isolation. Vehicle, concentration, and the presence of other ingredients all influence whether a given product will actually induce comedones on human skin."
A comedogenic ingredient checker worth shipping cannot rely on a hand-maintained list. It needs a versioned, source-linked, region-aware backend — which is precisely what a structured comedogenicity API is for. The next section defines the contract that backend must honor for acne-prone ingredient detection to work at production scale.
The Data Model: Fields a Comedogenic Ingredient API Must Return
Before designing the request-response loop, clarify what fields the API contract guarantees. This is the schema you're coding against, and the schema your UI components will bind to. Dermalytics indexes 25,000+ ingredients normalized from FDA, EU CosIng, and Health Canada — the field design reflects that source variability and the structural needs of INCI list analysis at scale.
comedogenicity_score(integer, 0–5). Maps to the Fulton scale (Fulton, 1989). 0 = non-comedogenic, 5 = highly comedogenic. Treat this not as opinion but as an evidence-weighted score with provenance back to a citable source. Your application logic switches on this integer; your UI never displays it raw.irritancy_score(integer, 0–5). Separate from comedogenicity. An ingredient can be non-comedogenic but irritating (many essential oils), or comedogenic but non-irritating (refined coconut derivatives). According to Basketter DA et al., Cutaneous and Ocular Toxicology 2014, HRIPT protocols test at 0.2–2% under occlusion across multiple weeks — irritancy is methodologically distinct from comedogenicity testing and deserves its own field.safety_status(enum: approved | restricted | banned | under_review). Derived from EU Annex II/III status under Reg. 1223/2009. "Banned" reflects Annex II's 1,600+ substances; "restricted" reflects Annex III with conditional concentration limits or product-type constraints; "under_review" surfaces ingredients currently the subject of an active SCCS opinion process.cas_number/ec_number/inci_name. Identifiers normalized from CosIng. Use CAS as the primary key when synonym ambiguity exists. "Talc" versus "talcum powder," "tocopherol" versus "vitamin E," "aqua" versus "water" — all map to single CAS-keyed entities. Your client code should query by CAS where available, by INCI name as fallback.max_concentration_eu(float, % w/w). EU regulatory ceiling for restricted substances. Salicylic acid (Annex III, ref. 98) is capped at 3.0% in rinse-off hair products, 2.0% in other rinse-off, and 0.5% in body lotions. A U.S.-tuned app that ignores this field will mislabel EU formulations.severity_label(string). A human-readable derived field ("low," "moderate," "high"). This is what tooltips render. The integer score is what your logic switches on. Keep them separate — they evolve on different cadences.

For quick reference when designing your ingredient severity scoring layer, the Fulton scale maps as follows:
| Score | Fulton label | Typical interpretation | Suggested user-facing label |
|---|---|---|---|
| 0 | Non-comedogenic | Safe in standard formulations | Safe |
| 1 | Slightly comedogenic | Low risk, vehicle-dependent | Safe |
| 2 | Mildly comedogenic | Low–moderate; concentration matters | Generally safe |
| 3 | Moderately comedogenic | Caution for acne-prone | Caution |
| 4 | Fairly comedogenic | Avoid for acne-prone in leave-ons | Avoid (acne-prone) |
| 5 | Highly comedogenic | Historic high-risk; verify formulation | Avoid |
This table is a reference, not a rule. The section on score-to-label translation covers why hard-coding these mappings is a trap — and what to do instead.
Choosing Between GET Single-Ingredient and POST Batch Endpoints
Most teams default to one endpoint pattern and live with the consequences. The choice has direct impact on credit usage, latency, UX flow, and what features become feasible. Frame this as an architecture-fit question: what is your product surface?
| Dimension | GET /v1/ingredients/{name} | POST /v1/analyze |
|---|---|---|
| Use case | Single ingredient detail, tooltip, real-time scan | Full INCI list, formulation analysis, catalog enrichment |
| Latency target | Sub-50ms median | Sub-100ms median |
| Payload | URL parameter (ingredient name) | JSON array, optional concentrations |
| Credit model | Per successful match | Per matched ingredient in batch |
| Best for | Encyclopedias, education flows | Scanners, PDPs, compliance exports |
The latency reasoning traces to Google's RAIL model: interactions feel "instantaneous" under 100ms and become perceptibly delayed beyond that threshold. A scanner app issuing 30 sequential GET calls for a 30-ingredient INCI list will hit ~1,500ms cumulative latency even at sub-50ms per call — well past user patience. The batch formulation analyzer at POST /v1/analyze collapses that into a single sub-100ms round trip. For any scan-based UX, this is not a preference; it is the only sustainable choice.
Batch is not always correct, though. If your product is an ingredient encyclopedia — tap an ingredient, see its full profile — single-ingredient GET calls give you cleaner caching semantics, more granular credit accounting, and easier CDN-level cache hits on popular ingredients. Beauty tech startups building education-first apps frequently choose GET; ingredient scanners and e-commerce PDPs nearly always choose POST. REST API skincare data consumption patterns split along this axis cleanly.
A credit-efficiency note: under the charged-on-successful-match model, batch calls do not penalize you for unmatched ingredients in the array — only matched items count against your quota. For INCI list analysis where partial matches on rare botanicals are routine, this makes batch the economically dominant choice. Pick the endpoint that matches the granularity of the user's question, not the granularity of your data model.
Wiring the Request-Response Loop: From INCI Input to Scored Output
Architecture is settled; data model is settled. Now the wiring.
Step 1 — Normalize the input
INCI lists arrive from OCR, user paste, or product database imports with inconsistent capitalization, synonyms, and Latin botanical names. The comedogenicity API auto-maps common synonyms ("talc" ↔ "talcum powder," "tocopherol" ↔ "vitamin E"), but your client should still pre-process: lowercase, strip parenthetical descriptors ("Glycerin (vegetable)" → "glycerin"), and split comma-separated INCI strings. With 25,000+ indexed ingredients you still have a long tail of rare botanicals. Step 4 handles the misses.
Step 2 — Structure the POST payload
POST /v1/analyze
{
"formulation": [
{"name": "aqua"},
{"name": "glycerin", "concentration": 5.0},
{"name": "isopropyl myristate", "concentration": 3.0},
{"name": "salicylic acid", "concentration": 0.5}
],
"region": "EU"
}
The optional concentration field matters more than most teams realize. CIR safety assessments document typical use levels: isopropyl myristate at 2–10% in leave-ons, cetearyl alcohol at 1–5%, coconut oil up to 20–30% in balms. Passing concentration lets the API return a concentration-aware severity rather than a worst-case default. If you do not pass concentration, you are asking the API to assume the upper-bound use case for every ingredient — and that assumption is what manufactures false positives in your UI.
Step 3 — Parse the response
{
"results": [
{
"name": "isopropyl myristate",
"cas_number": "110-27-0",
"comedogenicity_score": 5,
"irritancy_score": 2,
"safety_status": "approved",
"severity_label": "highly comedogenic",
"max_concentration_eu": null,
"synonyms": ["IPM", "isopropyl tetradecanoate"]
}
],
"formulation_summary": {
"max_comedogenicity": 5,
"avg_comedogenicity": 2.1,
"restricted_count": 0,
"banned_count": 0
}
}
formulation_summary is what most UIs render as the top-level product badge — a single verdict per product. Individual results[] entries power the drill-down view when the user taps for detail. Render both layers. Users who only want a green/yellow/red signal stop at the summary; users who want to understand why drill into the array.
Step 4 — Handle edge cases
Four cases every implementation must handle explicitly:
- 404 / not_found: ingredient not indexed. Render "not yet indexed" — never fabricate a score.
- 422 / malformed payload: missing
formulationarray, wrong types. Surface to logs, not to users. - 403 / region_restricted: the ingredient is banned in the requested
region. Render the regulatory flag, not just a score. comedogenicity_score: null: insufficient evidence. Treat as "unknown," never as "safe."
The last case is the one that quietly ships to production wrong. A null is not a 0. If your front-end logic uses score < 3 to label "safe," null will coerce to falsy and slip through. Switch on score === null first, always.
Step 5 — Cache aggressively
Comedogenicity data updates on regulatory cycles — quarterly at most, since SCCS opinions feed CosIng updates several times per year and CIR monographs update on multi-year cadences. Cache responses for 24h–7d depending on traffic. The official SDKs (published on npm and PyPI) ship with built-in cache invalidation tied to the API's data_version response header — when the header changes, your cache flushes. Roll your own at your peril; synonym mapping and version awareness are the two things that quietly drift in hand-written HTTP clients.
Normalized ingredient names and auto-mapped synonyms mean your app never has to say "ingredient not found" when the data exists — just under a different name.
Translating 0–5 Scores into User-Facing Labels Without Lying
The API returns a 0–5 integer. The user sees "safe" or "avoid." The mapping layer between those two is where most comedogenic ingredient checkers go wrong, and where liability quietly hides.
Whitney Bowe, board-certified dermatologist, put it plainly to The New York Times: "'Non-comedogenic' is not a tightly regulated term. A product can be labeled non-comedogenic and still break out an acne-prone patient, especially if it's occlusive or fragranced. Patch-testing and real-world feedback are just as important as the ingredient list" (Bowe, quoted in The New York Times, "Are Your Skin-Care Products Causing Acne?" 2019). Rachel Nazarian, of Schweiger Dermatology Group, reinforced the point in Allure (2020): "Some ingredients that are on old 'comedogenic lists' have been largely exonerated in modern formulations. It's not just what's in there, it's how much and what else is in the formula."
The API returns a score. Your product returns a verdict. Confusing the two is how trust gets quietly broken.
The decision matrix below shows the four common threshold strategies and what each costs you:
| Threshold tier | Score → Label mapping | Best for | Trade-off |
|---|---|---|---|
| Conservative | 0–1 safe; 2–3 caution; 4–5 avoid | Acne-prone-only apps | High false-positive rate |
| Balanced | 0–2 safe; 3 caution; 4–5 avoid | General consumer scanners | Matches modern derm consensus |
| Liberal | 0–3 safe; 4 caution; 5 avoid | DTC brands with broad catalogs | Higher user-complaint risk |
| Personalized | Threshold shifts per user skin profile | Apps with onboarding | Higher engineering cost |

Hard-coding a single threshold is risky because the population is not uniform. The American Academy of Dermatology reports that acne affects up to 50 million Americans annually and that 85% of people aged 12–24 experience at least minor acne. An app serving teen acne sufferers needs the conservative tier. An app serving general wellness shoppers needs balanced. A DTC brand cannot afford to flag its own bestsellers without context. Same API, three threshold layers, three viable products.
Personalization is the pattern that ages best. If your app collects skin type at onboarding (oily, dry, combination, sensitive, acne-prone), use that signal to swap threshold tiers per user at render time. Store the raw API scores in your database; never store the rendered label. Labels should be recomputed when a user updates their profile — otherwise your "safe" badge from six months ago lies the moment your user marks themselves acne-prone.
Regional variance is the second axis. The EU's restricted-ingredient list under Annex III imposes hard concentration ceilings with no U.S. equivalent. A salicylic acid body lotion at 1% is restricted in the EU (max 0.5%) but legal in the U.S. Your label logic must check safety_status and region before rendering "safe":
if result.safety_status == "banned" in region:
label = "not permitted"
elif result.comedogenicity_score >= user.threshold_avoid:
label = "avoid"
elif result.comedogenicity_score >= user.threshold_caution:
label = "caution"
else:
label = "safe"
The label is your product's voice. The score is the API's evidence. Keep them on separate layers and your comedogenic ingredient checker can evolve its UX without re-shipping its data — and your acne-prone ingredient detection logic can specialize per-user without you ever touching the backend contract.
Three Production Architecture Patterns for Shipping the Checker
Most teams discover the right architecture in production after the wrong one. Three patterns survive scale. Each has honest trade-offs.
- Pattern A — Client-Side Direct Call (Mobile Scanner Apps). The mobile app calls the API directly from the device after OCR or barcode scan. Auth via short-lived JWT issued by your backend; never embed long-lived keys. Pro: lowest backend load, transparent latency, offline-degraded behavior via SDK cache. Con: requires SSL pinning, credential rotation, and client-side rate-limit handling. Best for: standalone B2C ingredient scanners and mobile-first acne-prone ingredient detection apps. Latency profile: dominated by mobile network — a sub-100ms API call wraps inside a 200–400ms total user-perceived round trip.
- Pattern B — Backend Proxy (E-Commerce and SaaS). Your server calls the API; returns pre-shaped JSON to your frontend. Add business logic in the proxy: brand filters, internal product taxonomy, A/B-tested labels. Pro: single source of truth, easier observability, one place to swap providers. Con: one extra network hop; your infrastructure becomes the SLA-critical layer. Best for: e-commerce PDPs, compliance dashboards, internal B2B tooling. This is where the API's 99.9% uptime SLA matters most — your proxy inherits that floor but cannot exceed it without its own redundancy.
- Pattern C — Batch Enrichment (Catalog Pre-Scoring). Nightly or weekly job iterates over your product catalog, calls the batch formulation analyzer per product, persists scores to your database. Render product pages from your DB; zero runtime API latency. Pro: zero user-facing latency, full audit trail, deterministic at render time. Con: staleness window between syncs; you must subscribe to API change webhooks or the
data_versionheader to know when to re-sync. Best for: catalogs of 1,000+ products, CMS-driven sites, compliance teams who need versioned exports. Sync cadence: align with EU regulatory update frequency — several SCCS opinions per year means quarterly is the defensible baseline, monthly is safer for compliance-heavy markets.
Most teams ship Pattern A first because it's the shortest path to a demo, then migrate to Pattern B once they need server-side business logic. Pattern C is the right choice for catalog-driven products from day one — retrofitting it after launch is expensive. Hybrids are common: many beauty tech startups run Pattern C for their owned catalog plus Pattern A for user-scanned arbitrary products. The hybrid lets you render PDPs from your database (fast, audited) while still answering ad-hoc user scans against the live API.
The best architecture is not the most sophisticated — it is the one that lets your users get ingredient clarity without waiting.

Pitfalls That Break Comedogenic Checkers in Production
Every pitfall here has shipped to production somewhere. The API's data model is designed so you can avoid most of them — if you read the contract.
- Pitfall 1: Treating comedogenicity and irritancy as the same field. How it breaks: an ingredient with
comedogenicity_score: 1andirritancy_score: 4(certain essential oils, for example) gets labeled "safe" by acne logic, then triggers contact dermatitis complaints from sensitive users. How the API helps: separatecomedogenicity_scoreandirritancy_scorefields force independent decisions. Your label logic should switch on both. As Basketter (2014) documents in the HRIPT literature, irritancy is methodologically distinct from comedogenicity — one tests for pore-occluding cellular response, the other for cumulative contact reaction at 0.2–2% under occlusion. - Pitfall 2: Ignoring concentration. How it breaks: you flag a moisturizer containing 0.1% of a "comedogenic" ingredient the same as a balm containing 20%. Cosmetic chemist Perry Romanowski put it directly on The Beauty Brains (2016): "Those comedogenicity lists are mostly based on rabbit ear testing done decades ago, which does not always correlate well with real-world human usage. They're oversimplified if you don't consider concentration and the rest of the formula." How the API helps: the batch endpoint accepts optional
concentrationper ingredient and returns severity adjusted for typical use ranges sourced from CIR. - Pitfall 3: Trusting "non-comedogenic" claims on product labels. How it breaks: you ingest manufacturer marketing copy and surface it as truth. The FDA explicitly does not regulate "non-comedogenic" claims. Manufacturers can use the term without standardized testing. How the API helps: scoring derives from regulatory sources (FDA, EU CosIng, Health Canada) and historical testing data, not from product marketing copy. Your checker becomes an auditing layer on manufacturer claims, not a relay for them.
- Pitfall 4: No graceful handling of unmapped ingredients. How it breaks: a rare botanical returns null, your code falls through to "safe" by default, and an acne-prone user breaks out. Static checkers like Natural Acne Clinic's tool and Face Reality's checker often default to binary outputs that treat any presence of a flagged ingredient as unsafe — the inverse failure mode. Both extremes mislead. How the API helps: explicit
nullandinsufficient_dataresponses force your code to handle the unknown. Render "not yet indexed" — never default to "safe" or "unsafe" silently. - Pitfall 5: One global threshold for all users and regions. How it breaks: you ship a U.S.-tuned threshold to EU users and miss Annex III restrictions. Salicylic acid at 2% in a body lotion is legal under U.S. logic, restricted at 0.5% in the EU. How the API helps: the
regionparameter on the request returns region-awaresafety_status. Pair it with user skin-profile thresholds for the per-user, per-region matrix your real product needs.
Pre-Ship Checklist for a Production Comedogenic Ingredient Checker
Before you push the comedogenic ingredient checker to your app store listing or PDP, walk this list. Each item maps to a section above; each is something you can verify today, not eventually.
- Confirm your endpoint choice matches your UX granularity.
GET /v1/ingredients/{name}for education flows;POST /v1/analyzefor full INCI list analysis. Mixing them is fine; defaulting to the wrong one is expensive. - Pass
regionon every request. EU and U.S. regulatory contexts diverge — Annex III restrictions have no U.S. analog. A missing region parameter is a silent bug. - Pass
concentrationwhenever you have it. A 0.1% ingredient is not a 5% ingredient. CIR-derived defaults are conservative; your data, when you have it, is better. - Store the raw score, render the label. Persist
comedogenicity_scoreandirritancy_scoreseparately; recompute the user-facing label at render time from the user's threshold tier. Never persist the rendered label — it goes stale the moment the user updates their profile. - Handle
nullandinsufficient_dataexplicitly. Never default to "safe" on unknown ingredients. Render "not yet indexed." Switch onnullbefore any numeric comparison. - Wire your cache invalidation to the API's
data_versionheader. Quarterly sync is the defensible floor; monthly is safer for compliance-heavy markets. - Separate comedogenicity and irritancy in your label logic. Two fields, two decisions. An ingredient can be safe on one axis and risky on the other.
- Use the official npm or PyPI SDK. Do not hand-roll HTTP. You will get cache invalidation and synonym mapping wrong, and the bug will not surface until your user count crosses five figures.
- Instrument response-time monitoring. Per Google's RAIL guidelines, sub-100ms is "instantaneous." If your p95 drifts above 200ms, the bottleneck is on your side — start with serialization and ORM overhead before blaming the network.
- Document what the label means in your app. Your "caution" is not the dermatology definition — it is your product's interpretation. Make this internal documentation explicit so design, support, and legal share one definition when a user emails to complain.
FAQ: Implementation Questions Developers Actually Ask
Q1: Should I use the API's severity_label directly, or build my own?
The API's severity_label ("highly comedogenic," "low irritancy") is dermatologically accurate but reads as clinical. Most teams render their own labels — "safe / caution / avoid" — tuned to their audience's literacy and liability posture. Store both. Show your label in the primary UI; expose the API's label on the detail drill-down for transparency. Users who want to know what the underlying database actually said deserve to see it.
Q2: What happens when an ingredient isn't in the 25,000+ indexed set?
The API returns comedogenicity_score: null with a status indicator. Your app should render "not yet indexed" and either skip scoring for that ingredient in the aggregate or prompt the user to flag it for review. Never fall back to a third-party static list silently — that reintroduces the source-conflict problem covered earlier. If you must show something, show the absence honestly.
Q3: How often do I need to refresh cached scores?
EU CosIng updates incorporate SCCS opinions several times per year; CIR monographs update on multi-year cadences. A quarterly sync is the defensible floor. The SDK's data_version header tells you when a re-sync is warranted before quarterly — subscribe to it and your cache stays honest without manual intervention.
Q4: Can I use the data on consumer-facing product labels or marketing?
Check your plan's terms. Most tiers allow white-label use for end-user-facing app features (badges, tooltips, PDP labels). Reselling the raw dataset, or republishing the full database, is typically restricted. Attribution is appreciated, not legally required on most tiers — consult your contract before you put a logo on a billboard.