Overview
The MaIA API is the HTTPS gateway B4A exposes at maiascan.ai for the photo-based MaIA products. You authenticate with a key from your account, send a photo as multipart/form-data and get the answer synchronously: a beauty profile in JSON (MaIA Scan — skin tone and undertone, hair colour and hair type, choosing which models to run) or the same photo as a PNG with the product applied (MaIA Try-On — lipstick in preset shades or any colour of your brand, and foundation as a makeup simulator). The models are experimental: the response carries a status per attribute and flags when the person should confirm the result.
Nothing is assumed about your stack: any HTTP client that can send multipart works. No mandatory SDK, no webhook, no session state — the response of the call is the result. The analysis is cosmetic and photo-based: it is not a diagnosis, does no facial recognition and does not identify people.
| Product | Availability | Endpoint | Description |
|---|---|---|---|
| MaIA Scan | Self-service via API | POST /api/v1/scan | Skin and hair profile in JSON from a front-facing selfie (optional side photo): skin tone and undertone, hair colour and hair type. You choose which models to run in the models field. |
| MaIA Try-On — lipstick | Self-service via API | POST /api/v1/try-on | The customer's photo comes back as a PNG with the lipstick applied: six preset shades (shade) or any colour of your brand as #RRGGBB (color), with matte, satin or gloss finish and adjustable intensity. |
| MaIA Try-On — foundation (makeup simulator) | Self-service via API | POST /api/v1/try-on | category=foundation: the same photo with a foundation in the colour you send (#RRGGBB), with adjustable intensity. Same endpoint, same key. |
| MaIA Beauty Advisor | As a project | — | Not a self-service endpoint. Delivered by B4A as a project — on the brand's WhatsApp, as a product-page widget or in chat — on top of the same profile data MaIA Scan returns and your brand's integrated catalogue. |
Pricing
You buy credits in packages and spend them across the scan and the try-on. Credits never expire. Prices below are in US dollars for companies outside Brazil, paid by card; Brazilian companies pay in reais (R$ 1,998, R$ 4,998 and R$ 24,998) by card, Pix or boleto in our own checkout.
| Package | Price | Credits | Per credit |
|---|---|---|---|
| Starter | US$ 398 | 2,500 | US$ 0.16 |
| Growth | US$ 998 | 10,000 | US$ 0.10 |
| Scale | US$ 4,998 | 60,000 | US$ 0.08 |
One-off and automatic top-ups follow the per-credit rate of your last paid package. Enterprise volumes: talk to us.
How many credits each operation costs is listed in your account area and in the documentation PDF, available to any login — even before the first purchase.
Start in four steps
Create the account
Sign in with Google, e-mail and password, or receive a magic link by e-mail. No manual approval — and the full documentation PDF, with the price list, is already available in your account.
Create accountBuy a credit package
Starter (2,500 credits), Growth (10,000) or Scale (60,000) — by card in US dollars, or in reais by card, Pix or boleto for Brazilian companies. Credits never expire.
See packagesCreate the API key
Key creation unlocks after the first purchase. The key (msk_live_…) is shown once — copy it into your secrets vault.
My accountMake the first call
Send a selfie to POST /api/v1/scan (choosing the models in models) or a photo with shade/color, finish and intensity to POST /api/v1/try-on. The answer arrives in the same request.
curl -X POST https://maiascan.ai/api/v1/scan \
-H "Authorization: Bearer msk_live_…" \
-H "x-request-id: order-8841" \
-F "front=@selfie.jpg;type=image/jpeg" \
-F "side=@profile.jpg;type=image/jpeg" \
-F "models=skin,hair_color,hair_type"
# models is optional: omit it for the complete scan, or send e.g. "models=skin"Authentication
Every route under /api/v1 requires the Authorization header with an API key in Bearer format. The key identifies the account (and therefore the credit balance) and the key itself — usage is recorded per key. Your key never leaves the gateway: calls to the model are made with B4A's credential, on behalf of your account.
Authorization: Bearer msk_live_…- Format: msk_live_ followed by 32 alphanumeric characters. We store only the SHA-256 hash and a display prefix; the full text is shown once, at creation.
- Up to 10 active keys per account. Revoke any key at any time in the portal (My account → API keys); revocation applies to the next call.
- Missing, invalid or revoked key: 401 with { "error": "invalid_api_key" }.
- Use the key on the server only. Never embed it in browser code or a distributed app — whoever holds the key spends your credits.
- Optional header x-end-user-id (up to 160 characters: letters, digits and @ . _ : + -): your end customer's identifier, forwarded to the model for your own reconciliation. Outside that format it is ignored, never rejected.
Credits
Credits are consumed per operation: a MaIA Scan analysis charges for the models you chose to run (and only those that returned a value), and every image rendered by MaIA Try-On — lipstick or foundation — charges its operation. The credit is reserved before we call the model and settled when the answer arrives; if the model fails, or a scan model cannot predict anything, the corresponding part is refunded automatically. You never pay for a result you did not receive, and we never process a photo we cannot charge.
- The balance after the call comes with every successful response: credits_charged and credits_remaining in the scan JSON and the X-Credits-Charged and X-Credits-Remaining headers on both endpoints.
- Insufficient balance: the call is refused before any processing with 402 and { "error": "insufficient_credits", "balance": 2, "required": 3 }. The attempt is recorded in your usage, without a charge.
- Auto top-up: in the portal you set a threshold and an amount; when the balance drops below it, we create an order and e-mail the payment link (we never charge the card without you).
Price list and documentation PDF
The credits-per-operation table and the PDF with this whole documentation — including the attribute vocabularies and ready-to-run examples — live in your account, no purchase needed.
POST/api/v1/scan
Send a front-facing selfie (and optionally a side photo) and receive the person's beauty profile in JSON: skin tone and undertone, hair colour and hair type — only the models you ask for. Synchronous: the response is the result.
Request
multipart/form-data body with the fields below (unknown text fields are refused with 400). Headers: Authorization (required), x-request-id and x-end-user-id (optional).
| Field | Type | Required | Description |
|---|---|---|---|
front | file | yes | Front-facing photo of the face. Content-Type image/jpeg or image/png (we check the bytes too). Up to 6 MiB. Recommended short side of 480 px or more — below that the model may abstain. |
side | file | optional | Side (profile) photo of the same person, same types and limit. Sent in the same call at no extra cost; helps the hair models. |
models | text | optional | Which models to run, comma-separated: skin, hair_color, hair_type (any combination, no repeats). Omitted = the complete analysis with all three. Unknown value: 422 invalid_selection, nothing charged. |
x-request-id | header | optional | Your request identifier (up to 80 characters). Stored on the usage event for reconciliation; it does not deduplicate calls. |
x-end-user-id | header | optional | Your end customer's identifier (up to 160 characters, no spaces), forwarded to the model. Outside the format it is ignored. |
Models and attributes
Each model returns one or two attributes. The models field accepts the ids in the first column; the response carries, in result.attributes, exactly the attributes of the requested models.
| Model | Description | Attribute |
|---|---|---|
skin | Skin — Skin tone in nine levels (very_light … deep) and undertone (cool, neutral, warm). | skin_tone, skin_undertone |
hair_color | Hair colour — Nine categories, from black to light_blonde, plus red, gray_white and other. | hair_color |
hair_type | Hair type — Strand pattern: straight, wavy, curly or coily. | hair_pattern |
Response
200 OK, application/json. The envelope is fixed; result is the document returned by the model, passed through as-is. The same numbers come in the X-Session-Id, X-Request-Id, X-Credits-Charged and X-Credits-Remaining headers.
| Field | Type | Description |
|---|---|---|
session_id | string | Identifier of the usage event (the charged session). Keep it for support and reconciliation. |
request_id | string | Identifier of the request in the MaIA service. Quote both ids when talking to support. |
credits_charged | integer | Credits actually charged for this call — the models that returned at least one predicted attribute. |
credits_remaining | integer | Account balance after this call. |
models | string[] | Requested models, in canonical order (skin, hair_color, hair_type). |
models_delivered | string[] | Models that returned at least one attribute with status predicted — the ones that were charged. |
result.attributes.<attribute> | object | Per requested attribute: status (predicted, abstained or unsupported), value (the vocabulary value), score (uncalibrated softmax — not an accuracy), model_version, photo_roles and requires_confirmation. |
result.quality / result.experimental / result.requires_confirmation | object · boolean | Photo-quality diagnostics per role (front, side) and the model flags. The models are experimental: ask the person to confirm before using a value in a purchase decision. |
HTTP/1.1 200 OK
Content-Type: application/json
X-Session-Id: cmg1x3k9h0001abcd
X-Request-Id: 8f1c2e7a-…
X-Credits-Charged: 3
X-Credits-Remaining: 497
{
"session_id": "cmg1x3k9h0001abcd",
"request_id": "8f1c2e7a-…",
"credits_charged": 3,
"credits_remaining": 497,
"models": ["skin", "hair_color", "hair_type"],
"models_delivered": ["skin", "hair_color", "hair_type"],
"result": {
"attributes": {
"skin_tone": { "status": "predicted", "value": "light_medium", "score": 0.41, "score_kind": "uncalibrated_softmax_not_accuracy", "requires_confirmation": true, "photo_roles": ["front"] },
"skin_undertone": { "status": "predicted", "value": "warm", "score": 0.52, "requires_confirmation": true },
"hair_color": { "status": "predicted", "value": "dark_brown", "score": 0.63, "requires_confirmation": true },
"hair_pattern": { "status": "abstained" }
},
"quality": { "front": { … } },
"requires_confirmation": true,
"experimental": true
}
}Status of each attribute
A 200 may contain abstentions. Only what was predicted is charged:
| Status | Meaning |
|---|---|
predicted | The model returned a vocabulary value (value) with its score. The corresponding model is charged. |
abstained | The model was not confident enough on this photo (lighting, framing, resolution). Not charged. |
unsupported | The attribute cannot be assessed on this photo (for example, covered hair). Not charged. |
Errors
| Status | Code | Meaning | Credit charged? |
|---|---|---|---|
400 | front_required | The front field was not sent. | no |
400 | — | Unknown text field (body validation). | no |
401 | invalid_api_key | Missing, invalid or revoked key. | no |
402 | insufficient_credits | Balance below what the requested models need; the body carries balance and required. | no |
413 | photo_too_large | Image above 6 MiB. Uploads far above the limit are cut at reception with a generic 413. | no |
415 | unsupported_media_type | Content-Type of one of the files other than image/jpeg or image/png. | no |
422 | unsupported_photo | The bytes are not JPEG or PNG (or the file is empty). | no |
422 | invalid_selection | models contains an unknown id. | no |
422 | bad_photo | The model refused the photo (no face detected, photo too small). Credits refunded. | no |
422 | unusable_photo | No attribute could be predicted (all abstained or unsupported). Everything refunded; the body carries session_id and request_id. | no |
429 | rate_limited | More than 20 requests per minute with the same key (or the service is at its limit). Retry-After header in seconds. | no |
503 | upstream_unavailable | The model did not answer (failure or 180 s wait). Credits refunded; the body carries session_id. | no |
503 | upstream_not_configured | The analysis service is not connected on this environment. Nothing is charged. | no |
Limits
- Up to 6 MiB per file, at most 2 files (front + side), static JPEG or PNG.
- Synchronous call; the gateway waits for the model for up to 180 s. Give your HTTP client at least 200 s.
- 20 requests per minute per API key and 120 per minute per source IP.
Consent is mandatory: obtain the person's explicit authorisation before sending the photo (LGPD). MaIA Scan does no facial recognition or identification; it returns a cosmetic, experimental analysis for personalisation — not a diagnosis. Fields with requires_confirmation must be confirmed with the person.
Examples
curl -X POST https://maiascan.ai/api/v1/scan \
-H "Authorization: Bearer msk_live_…" \
-H "x-request-id: order-8841" \
-F "front=@selfie.jpg;type=image/jpeg" \
-F "side=@profile.jpg;type=image/jpeg" \
-F "models=skin,hair_color,hair_type"
# models is optional: omit it for the complete scan, or send e.g. "models=skin"POST/api/v1/try-on
Send a front-facing photo with the product to apply and receive the same photo as a PNG. Lipstick (category=lipstick, default): a preset shade (shade) or any colour of your brand (color, #RRGGBB), with finish and intensity. Foundation (category=foundation): an #RRGGBB colour with intensity. Only the makeup area changes. Synchronous.
Request
multipart/form-data body with the fields below (unknown text fields are refused with 400). Headers: Authorization (required), x-request-id and x-end-user-id (optional).
| Field | Type | Required | Description |
|---|---|---|---|
photo | file | yes | Front-facing photo, JPEG or PNG (we check the bytes, not just the Content-Type). Up to 6 MiB; short side of at least 320 px; mouth visible for lipstick. |
category | text | optional | lipstick (default) or foundation. |
shade | text | optional | Lipstick: one of the preset shades — rose, red, berry, coral, plum or nude (see GET /api/v1/try-on/shades). Exactly one of shade and color. |
color | text | optional | Any colour as #RRGGBB (upper or lower case). Lipstick: alternative to shade, for your brand's palette. Foundation: required. |
finish | text | optional | Lipstick: matte (default), satin or gloss. Not applicable to foundation. |
intensity | text | optional | Integer from 0 to 100. Default 100 for lipstick and 70 for foundation. |
x-request-id | header | optional | Your request identifier (up to 80 characters), stored on the usage event. |
Your brand's colour
Instead of a preset shade, send color=#RRGGBB with your product's hex — the model applies exactly that colour, at the requested finish and intensity. This is the way to try your own palette without a calibration project.
curl -X POST https://maiascan.ai/api/v1/try-on \
-H "Authorization: Bearer msk_live_…" \
-F "photo=@selfie.jpg;type=image/jpeg" \
-F "color=#993355" \
-F "finish=satin" \
-F "intensity=85" \
-o try-on.png
# exactly one of shade / color; X-TryOn-Color echoes the normalised colour (#993355)Foundation (makeup simulator)
category=foundation applies a foundation in the colour given in color, with intensity (default 70). There is no shade or finish for foundation. Same key, same endpoint, same receipt in the headers.
curl -X POST https://maiascan.ai/api/v1/try-on \
-H "Authorization: Bearer msk_live_…" \
-F "photo=@selfie.jpg;type=image/jpeg" \
-F "category=foundation" \
-F "color=#C68642" \
-F "intensity=70" \
-o foundation.png
# no shade / finish for foundation; X-TryOn-Category: foundationResponse
200 OK, image/png — the body is the rendered image. The receipt travels in the headers:
| Header | Description |
|---|---|
X-Session-Id | Identifier of the usage event (the charged session). |
X-Request-Id | Identifier of the request in the MaIA service. |
X-Credits-Charged | Credits charged for this image. |
X-Credits-Remaining | Account balance after this call. |
X-TryOn-Category | the category rendered: lipstick, foundation, blush, … or full_look. |
X-TryOn-Color | The colour applied, as upper-case #RRGGBB — the preset's hex or your normalised colour. |
X-TryOn-Finish | The finish applied (lipstick only). There is no fallback: what you asked for is what was applied. |
X-Model-Version | Version of the render model, when reported. |
Cache-Control | no-store. The image is not kept in any cache on our side. |
HTTP/1.1 200 OK
Content-Type: image/png
Cache-Control: no-store
X-Session-Id: cmg1x4p2r0002efgh
X-Request-Id: 2b7d9f10-…
X-Credits-Charged: 1
X-Credits-Remaining: 496
X-TryOn-Category: lipstick
X-TryOn-Color: #B85C75
X-TryOn-Finish: gloss
X-Model-Version: lipstick-custom-srgb-v1
<PNG bytes>Errors
| Status | Code | Meaning | Credit charged? |
|---|---|---|---|
400 | photo_required | The photo field was not sent. | no |
400 | — | Unknown or malformed text field (body validation). | no |
401 | invalid_api_key | Missing, invalid or revoked key. | no |
402 | insufficient_credits | Insufficient balance; the body carries balance and required. | no |
413 | photo_too_large | Photo above 6 MiB. Uploads far above the limit are cut at reception with a generic 413. | no |
422 | invalid_selection | category, shade, color, finish or intensity outside the contract (e.g. shade and color together, colour not #RRGGBB, foundation without color). The body says which. | no |
422 | unsupported_photo | The bytes are not JPEG or PNG (or the file is empty). | no |
422 | bad_photo | The model refused the photo (face not frontal, mouth not visible, short side below 320 px). Credit refunded. | no |
429 | rate_limited | More than 20 requests per minute with the same key (or the service is at its limit). Retry-After header in seconds. | no |
503 | upstream_unavailable | The render service failed or did not answer within 180 s. Credit refunded; try again in a moment. | no |
503 | upstream_not_configured | The render service is not connected on this environment. Nothing is charged. | no |
Limits
- JPEG or PNG, up to 6 MiB, short side ≥ 320 px.
- Synchronous call; the gateway waits for the model for up to 180 s. Give your HTTP client at least 200 s.
- 20 requests per minute per API key and 120 per minute per source IP.
- Simulated colour may differ from the real product: the render is experimental and works in sRGB from the given hex. Calibrated palette with human validation and other makeup categories: as a project.
Examples — lipstick with a preset shade
curl -X POST https://maiascan.ai/api/v1/try-on \
-H "Authorization: Bearer msk_live_…" \
-F "photo=@selfie.jpg;type=image/jpeg" \
-F "shade=rose" \
-F "finish=gloss" \
-F "intensity=90" \
-D headers.txt \
-o try-on.png
# headers.txt now holds X-Session-Id, X-Request-Id, X-Credits-Charged, X-Credits-Remaining, X-TryOn-Color, X-TryOn-FinishGET/api/v1/try-on/shades
Lists what POST /api/v1/try-on accepts: the preset shades with their reference hex, the finishes, the custom-colour format, the intensity ranges, the categories and the upload ceiling. No credit consumed.
Response
200 OK, application/json, Cache-Control: private, max-age=3600. Requires the same key (401 without it).
| Field | Type | Description |
|---|---|---|
shades[].id | string | Value accepted in the try-on shade field. |
shades[].hex | string | Reference colour of the shade (#RRGGBB) to show in your UI. |
finishes | string[] | Values accepted in the finish field (lipstick). |
custom_color | boolean | true: the color field accepts any colour of your brand. |
color_format | string | Format accepted in color: #RRGGBB. |
intensity | object | Range and default of the lipstick intensity: { min, max, default }. |
foundation_intensity | object | Range and default of the foundation intensity: { min, max, default }. |
categories | string[] | Values accepted in category: lipstick, foundation. |
max_upload_bytes | integer | Photo upload ceiling in bytes (6 MiB). |
{
"shades": [
{ "id": "rose", "hex": "#B85C75" },
{ "id": "red", "hex": "#B8293D" },
{ "id": "berry", "hex": "#883252" },
{ "id": "coral", "hex": "#D96758" },
{ "id": "plum", "hex": "#703B5B" },
{ "id": "nude", "hex": "#AF786F" }
],
"finishes": ["matte", "satin", "gloss"],
"custom_color": true,
"color_format": "#RRGGBB",
"intensity": { "min": 0, "max": 100, "default": 100 },
"foundation_intensity": { "min": 0, "max": 100, "default": 70 },
"categories": ["lipstick", "foundation"],
"max_upload_bytes": 6291456
}Examples
curl https://maiascan.ai/api/v1/try-on/shades \
-H "Authorization: Bearer msk_live_…"Browser SDK (early access)
Beyond the photo API, MaIA Try-On exists as a browser SDK: thirteen simulators (lipstick, foundation, blush, powder, concealer, eyeshadow, mascara, eyebrows, eyeliner, bronzer, contour, highlighter and lip liner) and a Full Look, live through the camera or from a photo, rendered locally on the customer's device. No image is uploaded; your brand hosts the immutable bundle on its own origin.
The SDK never calls this API on its own. Lipstick and foundation can get a final server render — through the endpoint above, with the account key server-side — charged in credits like any call; the other products and Full Look are local previews. The integration guide (JavaScript, React, Angular, CSP, privacy, versions) lives in the signed-in area, and access is enabled account by account.
How to get the SDK today
- Create an account on maiascan.ai (e-mail or Google) and open My account → SDK.
- Click "Request access": the SDK is in early access and the B4A team enables it account by account.
- Once enabled, the same page shows the current version, its SHA-256 and the "Download bundle" button (package.tgz). Host the contents of package/ on your origin at /vendor/maia-try-on/<version>/ — the SDK needs same-origin workers, WASM and models.
- npm: once enabled, npm i @maiascan/try-on works through our private registry (maiascan.ai/api/npm), authenticated with the account\u2019s API key — the two .npmrc lines are in the signed-in area. No npmjs.com account is needed.
Reference: attribute vocabularies
The possible values of value in result.attributes, per attribute. Vocabularies are fixed per API version (api_version in the model envelope); new values arrive as a compatible change.
| Attribute | Possible values | Description |
|---|---|---|
skin_tone | very_light, light, light_medium, medium, medium_tan, medium_dark, dark, very_dark, deep | Nine skin-tone levels, from lightest to deepest. |
skin_undertone | cool, neutral, warm | Skin undertone. |
hair_color | black, dark_brown, medium_brown, light_brown, dark_blonde, light_blonde, red, gray_white, other | Predominant hair colour. |
hair_pattern | straight, wavy, curly, coily | Strand pattern. |
Every attribute comes with status predicted, abstained or unsupported; score is an uncalibrated softmax — treat it as a ranking, not as a probability or an accuracy.
Errors and status codes
Every error is JSON with a stable error field (for your code) and a readable message (for the log). Compare error, not message. No error consumes a credit: when the failure happens after the reservation (422 bad_photo or unusable_photo, 429, 503), the refund is written in the same transaction and the body carries session_id.
| Status | Meaning |
|---|---|
400 | Malformed request: required field missing or unknown text field. |
401 | invalid_api_key — missing, invalid or revoked key. |
402 | insufficient_credits — balance below what is needed; the body carries balance and required. |
413 | photo_too_large — image above 6 MiB. |
415 | unsupported_media_type — Content-Type other than image/jpeg or image/png (scan). |
422 | Valid in form, invalid in content: invalid_selection, unsupported_photo, bad_photo, unusable_photo (scan). |
429 | rate_limited — per-key or service limit; wait Retry-After seconds and retry with backoff. |
503 | upstream_unavailable (credit refunded) or upstream_not_configured (nothing charged). |
HTTP/1.1 402 Payment Required
Content-Type: application/json
{
"error": "insufficient_credits",
"message": "Buy a credit package to continue.",
"balance": 2,
"required": 3
}Limits, idempotency and x-request-id
- Rate limit: 20 requests per minute per API key on every /api/v1 route (429 rate_limited with Retry-After), plus 120 per minute per source IP. For higher volumes, talk to us.
- Idempotency: calls are not idempotent — every 200 consumes credits. Do not automatically retry a call that returned 200; retry only 429 and 503 errors (which do not charge), honouring Retry-After and with exponential backoff.
- x-request-id: send your identifier (up to 80 characters). It is stored on the usage event and shows in reconciliation; use it together with session_id / X-Session-Id and request_id / X-Request-Id when talking to support.
- Timeouts: the gateway waits for the model for up to 180 s. Give your HTTP client at least 200 s so it does not abandon a call that will still be charged and answered.
Data handling and consent
- Consent: you are responsible for obtaining the person's explicit authorisation before sending any photo, under the LGPD. In our own demo the text used is: "I authorise the processing of this photo only to generate the simulation; it is not stored."
- Photos in the gateway: they live in memory only for the duration of the request and are forwarded once to the model. The gateway writes no photos to disk, database or logs — logs record size, operation, latency, status and request identifiers.
- In the MaIA service: nothing is retained or used for training (retained: false, used_for_training: false in the envelope). Only the result — JSON or PNG — comes back to you, with Cache-Control: no-store.
- What we do not do: facial recognition, identification of people, inference of protected traits or medical diagnosis. The analysis is cosmetic and experimental; fields with requires_confirmation must be confirmed with the person.
- Usage log: per request we keep account, key, endpoint, status, credits, latency, x-request-id and the model's request_id — that is what shows on your usage page.
Base URL, version and contact
https://maiascan.ai/api- Base URL: https://maiascan.ai/api — always HTTPS. Public routes live under /v1.
- Versioning: the version goes in the path (v1). Compatible changes (new JSON fields, new headers, new shades, new vocabulary values) arrive without a version bump; any incompatible change is born as /v2, with /v1 kept in parallel for an announced period.
- Gateway status: GET https://maiascan.ai/api/health returns { "status": "ok", "commit": "…" } — no authentication.
- Technical and commercial support: comercial@b4a.ai.
Need more volume, higher per-key limits, a calibrated palette with human validation, other makeup categories, MaIA Beauty Advisor or an enterprise contract? Talk to us — B4A delivers complete projects on top of the same API.