Overview
The MaIA API is the HTTPS gateway B4A exposes at maiascan.ai for two of the three 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 as JSON (MaIA Scan) or the same photo as PNG with the product applied (MaIA Try-On). Every successful call consumes one credit from the account balance.
Nothing is assumed about your stack: any HTTP client that can send multipart works. There is no mandatory SDK, no webhook and no session state — the response of the call is the result.
| Product | Availability | Endpoint | Description |
|---|---|---|---|
| MaIA Scan | Self-service via API | POST /api/v1/scan | Skin and hair profile as JSON from a front selfie (side photo optional). 1 credit per analysis. |
| MaIA Try-On | Self-service via API | POST /api/v1/try-on | The customer's photo comes back as PNG with the lipstick applied — in this version, the six demo shades and three finishes. 1 credit per render. A palette calibrated to your products: as a project. |
| MaIA Beauty Advisor | As a project | — | Not a self-service endpoint. B4A delivers it as a project — on the brand's WhatsApp, as a product-page widget or in chat — using the same profile data MaIA Scan returns and your brand's integrated catalog. |
Start in four steps
Create the account
Sign in with Google or receive a magic link by e-mail. No password, no manual approval.
Create accountBuy a credit package
Starter (500 credits), Growth (2,500) or Scale (10,000) — 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 exactly once — copy it into your secrets vault.
My accountMake the first call
Send a selfie to POST /api/v1/scan, or a photo with a shade and a finish to POST /api/v1/try-on. The answer comes back 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"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.
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 from the next call.
- Missing, invalid or revoked key: 401 with { "error": "invalid_api_key" }.
- Use the key server-side only. Never embed it in browser code or a distributed app — whoever holds the key spends your credits.
Credits
1 credit = 1 successful call, whether an analysis (scan) or a render (try-on). The credit is reserved before we call the model and settled when the answer arrives; if the model fails, the reservation is refunded automatically — you never pay for a result you did not receive, and we never process a photo we cannot charge.
| Package | Price | Credits | per call |
|---|---|---|---|
| Starter | R$ 998 | 500 | R$ 2.00 |
| Growth | R$ 1,998 | 2,500 | R$ 0.80 |
| Scale | R$ 4,998 | 10,000 | R$ 0.50 |
One-off and automatic top-ups use the per-call price of the last paid package. Credits never expire. Enterprise: talk to us.
- The balance after the call comes with every successful response: credits_remaining in the scan JSON and the X-Credits-Remaining header on the try-on.
- Zero balance: the call is refused before any processing with 402 and { "error": "insufficient_credits", "balance": 0 }. The attempt is recorded in your usage, with no 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 a card without you).
POST/api/v1/scan
Send a front selfie (and optionally a side photo) and receive the person's beauty profile as JSON. Synchronous: the response is the result.
Production status (2026-09-22): the gateway is not yet connected to the MaIA Scan API analysis service. Until the credential is issued, POST /api/v1/scan answers 503 upstream_not_configured and no credit is charged. The contract in this section is final — integrate against it; the answer will become 200 with no change on your side.
Request
multipart/form-data body with the fields below. Headers: Authorization (required) and x-request-id (optional).
| Field | Type | Required | Description |
|---|---|---|---|
front | file | yes | Front photo of the face. Declared Content-Type must be image/jpeg, image/png, image/webp or image/heic. Up to 10 MB. |
side | file | optional | Side (profile) photo of the same person, same types and limit. Sent in the same call — no extra credit. |
x-request-id | header | optional | Your request identifier (up to 80 characters). Stored with the usage event for reconciliation; it does not deduplicate calls. |
Response
200 OK, application/json. The envelope is fixed; result is the analysis document returned by the MaIA Scan API.
| Field | Type | Description |
|---|---|---|
session_id | string | Identifier of the usage event (the charged session). Keep it for support and reconciliation. |
capture_id | string | Identifier of the capture at the MaIA Scan API. |
credits_remaining | integer | Account balance after this call. |
result | object | Beauty profile: skin tone and undertone, skin type, Skin Scan metrics (redness, oiliness, texture, pores, spots, hydration, pigmentation), hair color and structure, photo-quality checks and per-field confidence. Keys follow the MaIA Scan API version — treat it as a JSON document and read the fields you need. |
HTTP/1.1 200 OK
Content-Type: application/json
{
"session_id": "cmg1x3k9h0001abcd",
"capture_id": "cap_01J8…",
"credits_remaining": 499,
"result": { … }
}Errors
| Status | Code | Meaning | Credit charged? |
|---|---|---|---|
400 | front_required | The front field was not sent. | no |
400 | unsupported_media_type | Content-Type of one of the files outside JPEG/PNG/WEBP/HEIC. | no |
401 | invalid_api_key | Missing, invalid or revoked key. | no |
402 | insufficient_credits | Zero balance; the body carries balance. | no |
413 | — | File above 10 MB (refused on receipt; generic body). | no |
429 | — | More than 120 requests per minute from the same IP. | no |
502 | analysis_failed | The model did not complete the analysis. The reserved credit was refunded; the body carries session_id. | no |
503 | upstream_not_configured | The analysis service is not connected in this environment. Nothing is charged. | no |
Limits
- Up to 10 MB per file, at most 2 files (front + side).
- Synchronous call; the gateway waits for the model for up to 60 s.
- 120 requests per minute per source IP.
Consent is mandatory: obtain the person's explicit authorization before sending the photo (LGPD). MaIA Scan does no facial recognition or identification; it returns cosmetic analysis for personalization.
Samples
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"POST/api/v1/try-on
Send a front photo with a shade and a finish and receive the same photo as PNG with the lipstick applied. Only the makeup area changes. Synchronous.
Request
multipart/form-data body with exactly the fields below (unknown text fields are refused with 400). Headers: Authorization (required) and x-request-id (optional).
| Field | Type | Required | Description |
|---|---|---|---|
photo | file | yes | Front photo, JPEG or PNG (we check the bytes, not the Content-Type). Up to 6 MB; short side of at least 320 px; mouth visible. |
shade | text | yes | One of the demo shades: rose, red, berry, coral, plum or nude (see GET /api/v1/try-on/shades). |
finish | text | yes | matte, satin or gloss. |
x-request-id | header | optional | Your request identifier (up to 80 characters), stored on the usage event. |
Response
200 OK, image/png — the body is the rendered image. The receipt travels in headers:
| Header | Description |
|---|---|
X-Session-Id | Identifier of the usage event (the charged session). |
X-Credits-Remaining | Account balance after this call. |
X-TryOn-Finish-Requested | The finish you asked for. |
X-TryOn-Finish-Applied | The finish actually applied. When the model cannot apply the requested finish to a photo, the gateway renders at the shade's trained finish (matte for red/berry/plum, satin for rose/coral/nude) instead of failing — compare the two headers to tell the person. |
Cache-Control | no-store. The image sits in no cache on our side. |
HTTP/1.1 200 OK
Content-Type: image/png
Cache-Control: no-store
X-Session-Id: cmg1x4p2r0002efgh
X-Credits-Remaining: 498
X-TryOn-Finish-Requested: gloss
X-TryOn-Finish-Applied: gloss
<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 | Zero balance; the body carries balance. | no |
413 | photo_too_large | Photo above 6 MB. Uploads far above the limit are cut on receipt with a generic 413. | no |
422 | invalid_selection | shade or finish outside the list. | no |
422 | unsupported_photo | The bytes are neither JPEG nor 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 | — | More than 120 requests per minute from the same IP. | no |
503 | upstream_unavailable | The render service failed or is busy. Credit refunded; retry in a moment. | no |
503 | upstream_not_configured | The render service is not connected in this environment. Nothing is charged. | no |
Limits
- JPEG or PNG, up to 6 MB, short side ≥ 320 px. Photos larger than 1600 × 1079 px may be downscaled by the model before rendering.
- About 2 to 4 seconds per render. The render service scales to zero: the first call after an idle period can take longer (the gateway waits up to 60 s and retries a late cold-start 422 once).
- 120 requests per minute per source IP.
- Simulated color may differ from the actual product. In this API version the shades are the six demo shades; a palette calibrated to your brand's products is delivered as a project.
Samples
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" \
-D headers.txt \
-o try-on.png
# headers.txt now holds X-Session-Id, X-Credits-Remaining, X-TryOn-Finish-AppliedGET/api/v1/try-on/shades
Lists the shades and finishes accepted by POST /api/v1/try-on, with each shade's reference hex and trained finish. Consumes no credit.
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 color of the shade (#RRGGBB) to show in your UI. |
shades[].trained_finish | string | Finish the shade was trained with — the X-TryOn-Finish-Applied fallback. |
finishes | string[] | Values accepted in the finish field. |
max_upload_bytes | integer | Photo upload limit in bytes (6 MB). |
{
"shades": [
{ "id": "rose", "hex": "#B85C75", "trained_finish": "satin" },
{ "id": "red", "hex": "#B8293D", "trained_finish": "matte" },
{ "id": "berry", "hex": "#883252", "trained_finish": "matte" },
{ "id": "coral", "hex": "#D96758", "trained_finish": "satin" },
{ "id": "plum", "hex": "#703B5B", "trained_finish": "matte" },
{ "id": "nude", "hex": "#AF786F", "trained_finish": "satin" }
],
"finishes": ["matte", "satin", "gloss"],
"max_upload_bytes": 6291456
}Samples
curl https://maiascan.ai/api/v1/try-on/shades \
-H "Authorization: Bearer msk_live_…"Errors and status codes
Every error is JSON with a stable error field (for your code) and a readable message (for your logs). Compare error, not message. No error consumes a credit: when the failure happens after the reservation (502 on scan, 422 bad_photo or 503 on try-on), the credit is refunded in the same transaction.
| Status | Meaning |
|---|---|
400 | Malformed request: required field missing, file type not declared correctly or unknown text field. |
401 | invalid_api_key — missing, invalid or revoked key. |
402 | insufficient_credits — zero balance; buy a package or wait for the auto top-up. |
413 | File above the limit (10 MB on scan, 6 MB on try-on). |
422 | Well-formed input, invalid content: invalid_selection, unsupported_photo, bad_photo (try-on). |
429 | Requests-per-minute limit exceeded. Wait and retry with backoff. |
502 | analysis_failed — the analysis did not complete; credit refunded (scan). |
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": 0
}Limits, idempotency and x-request-id
- Rate limit: 120 requests per minute per source IP, on every /api/v1 route. There is no per-key limit yet; for higher volumes, talk to us.
- Idempotency: calls are not idempotent — every 200 consumes a credit. Do not automatically repeat a call that returned 200; retry only 429, 502 and 503 (which do not charge), with exponential backoff.
- x-request-id: send your identifier (up to 80 characters). It is stored on the usage event and shows up in reconciliation; quote it with session_id / X-Session-Id when talking to support.
- Timeouts: the gateway waits for the model for up to 60 s. Give your HTTP client at least 90 s so you do 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 authorization before sending any photo, under LGPD. In our own demo the wording is: "I authorize the processing of this photo only to generate the simulation; it is not stored."
- Photos at the gateway: they live in memory for the duration of the request and are forwarded once to the model. The gateway writes photos to no disk, database or log — logs carry size, shade, finish, latency and status.
- Try-on: the photo is stored neither by the gateway nor by the render service; only the PNG comes back to you, with Cache-Control: no-store.
- Scan: the MaIA Scan API stores the structured result with model and pipeline versions; raw-image retention is configurable per project — talk to us to set yours.
- What we do not do: facial recognition, identification of people, inference of protected traits or medical diagnosis. Low-confidence fields should be confirmed with the person.
- Usage record: per call we keep account, key, endpoint, status, credit, latency, x-request-id and, on scan, the capture_id — that is what your usage page shows.
Base URL, versioning and contact
https://maiascan.ai/api- Base URL: https://maiascan.ai/api — always HTTPS. Public routes live under /v1.
- Versioning: the version is in the path (v1). Compatible changes (new JSON fields, new headers, new shades) ship without a version change; 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, per-key limits, a palette calibrated to your catalog, MaIA Beauty Advisor or an enterprise contract? Talk to us — B4A delivers full projects on top of the same API.