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. |
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 (500 credits), Growth (2,500) or Scale (10,000) — pay by card, Pix or boleto in our own checkout. 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 current price list (how many credits each operation costs) and the complete technical documentation as a PDF live in the account area, available to any login — even before the first purchase.
| Package | Price | Credits |
|---|---|---|
| Starter | R$ 998 | 500 |
| Growth | R$ 1,998 | 2,500 |
| Scale | R$ 4,998 | 10,000 |
Credits never expire. One-off and automatic top-ups follow your last paid package. Enterprise: talk to us.
- 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. B4A internal teams: ask Jan directly.
- 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.