{"openapi":"3.1.0","info":{"title":"BUSE API","version":"1.0.0","summary":"Instant 3D-printing quotes and orders in Morocco","description":"BUSE prints parts in FDM in Agadir and delivers anywhere in Morocco, delivery included in every price.\n\nThe API is the one the website itself uses. Prices are computed on the server from the uploaded file's geometry: a client only chooses the material SKU, the finish, the quantity and whether the order is rushed, never a price, a volume or a weight.\n\nThe flow is: sign in (or sign up) → `POST /api/upload` the STL/OBJ/3MF → `POST /api/quotes/batch` to price a basket → `POST /api/addresses` → `POST /api/orders` → `POST /api/payments/checkout` for a Stripe Checkout URL. Every response is a JSON envelope `{ success, data }` or `{ success: false, error: { code, message, details } }`.\n\nEvery public HTML page also answers in Markdown with `Accept: text/markdown`; the site index for agents is /llms.txt.","contact":{"name":"BUSE","email":"contact@buse.ma","url":"https://buse.ma/en/contact"},"termsOfService":"https://buse.ma/en/terms"},"servers":[{"url":"https://buse.ma","description":"Production"}],"externalDocs":{"description":"Developer documentation","url":"https://buse.ma/en/developers"},"security":[{"sessionCookie":[]}],"tags":[{"name":"Auth","description":"Email + password sessions (better-auth). The session is a cookie; send it back on every call."},{"name":"Catalog","description":"Materials and delivery destinations."},{"name":"Quotes","description":"Upload a file, price it, save a devis."},{"name":"Orders","description":"Addresses, orders, payment, invoices."},{"name":"Projects","description":"Work the instant quote cannot price: a human quote."},{"name":"Cards","description":"The 3D-printed business card generator."},{"name":"Account","description":"Profile and contact."}],"paths":{"/api/auth/sign-up/email":{"post":{"summary":"Create an account","description":"Email, password and name. A verification email is sent; the session cookie is set once verified.","tags":["Auth"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":100},"email":{"type":"string","minLength":1,"maxLength":255,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"password":{"type":"string","minLength":8,"maxLength":100,"allOf":[{"pattern":"[A-Z]"},{"pattern":"[a-z]"},{"pattern":"[0-9]"}]},"confirmPassword":{"type":"string"},"phone":{"type":"string","pattern":"^(\\+212|0)(5|6|7)\\d{8}$"}},"required":["name","email","password","confirmPassword"]}}}},"responses":{"200":{"description":"Account created; see better-auth's response shape."},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/auth/sign-in/email":{"post":{"summary":"Sign in","description":"Sets the `better-auth.session_token` cookie on success. Keep the cookie jar for the calls below.","tags":["Auth"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","minLength":1,"maxLength":255,"format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"password":{"type":"string","minLength":1},"rememberMe":{"default":false,"type":"boolean"}},"required":["email","password"]}}}},"responses":{"200":{"description":"Signed in; the response carries the user and the cookie."},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Wrong email or password.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/auth/get-session":{"get":{"summary":"Current session","description":"Null when not signed in.","tags":["Auth"],"responses":{"200":{"description":"The session and user, or null."},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/auth/sign-out":{"post":{"summary":"Sign out","description":"Ends the session.","tags":["Auth"],"responses":{"200":{"description":"Signed out."},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/materials":{"get":{"summary":"Material catalog","description":"Every in-stock colour SKU with its price per gram. Public.","tags":["Catalog"],"security":[],"parameters":[{"name":"technology","in":"query","required":false,"schema":{"type":"string","enum":["fdm"]}}],"responses":{"200":{"description":"The catalog.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"materials":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"The SKU to send as `materialId`"},"name":{"type":"string"},"nameFr":{"type":"string"},"technology":{"type":"string","enum":["fdm"]},"materialType":{"type":"string","description":"Polymer family: pla, petg, abs, asa, tpu, ..."},"pricePerGram":{"type":"number","description":"MAD"},"density":{"type":"number","description":"g/cm³"},"available":{"type":"boolean"},"previewColor":{"type":"string"}}}}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/shipping/destinations":{"get":{"summary":"Delivery cities","description":"Type-ahead on the cities we deliver to; the id is what an address's `shippingDestinationId` takes. Rate limit: 60 per minute per client.","tags":["Catalog"],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":60}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":20,"default":10}}],"responses":{"200":{"description":"Matching cities.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"destinations":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"code":{"type":"string"},"name":{"type":"string"},"deliveryDaysMin":{"type":"integer"},"deliveryDaysMax":{"type":"integer"}}}}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/upload":{"post":{"summary":"Upload a 3D file","description":"Multipart form with one `file` field: STL, OBJ or 3MF, up to 50 MB. The returned `key` is what every pricing call takes as `fileKey`; it lives in the caller's own namespace. Rate limit: 20 per minute per client.","tags":["Quotes"],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["file"],"properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"Stored.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"key":{"type":"string"},"url":{"type":"string"},"metadata":{"type":"object","properties":{"filename":{"type":"string"},"size":{"type":"integer"}}}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"summary":"Upload limits","description":"Accepted extensions and the size cap.","tags":["Quotes"],"responses":{"200":{"description":"The limits.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"maxSize":{"type":"integer"},"maxSizeMB":{"type":"integer"},"allowedExtensions":{"type":"array","items":{"type":"string"}}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/analyze":{"post":{"summary":"Analyse a file and price one copy","description":"Geometry (volume, bounding box, printability) plus a quote in the given SKU. A part that does not fit the 256 mm bed, is too small or is not a closed mesh is a 400 with `details.reason`. Rate limit: 20 per minute per client.","tags":["Quotes"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"fileKey":{"type":"string","minLength":1},"filename":{"type":"string","minLength":1},"technology":{"default":"fdm","type":"string","enum":["fdm"]},"material":{"type":"string"},"finish":{"default":"standard","type":"string","enum":["standard","smooth","polished"]},"quantity":{"default":1,"type":"integer","exclusiveMinimum":0,"maximum":1000}},"required":["fileKey","filename"]}}}},"responses":{"200":{"description":"Geometry and price.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"analysis":{"type":"object"},"validation":{"type":"object"},"material":{"type":"object","properties":{"id":{"type":"string","description":"The SKU to send as `materialId`"},"name":{"type":"string"},"nameFr":{"type":"string"},"technology":{"type":"string","enum":["fdm"]},"materialType":{"type":"string","description":"Polymer family: pla, petg, abs, asa, tpu, ..."},"pricePerGram":{"type":"number","description":"MAD"},"density":{"type":"number","description":"g/cm³"},"available":{"type":"boolean"},"previewColor":{"type":"string"}}},"quote":{"type":"object","description":"What a part costs, as the checkout will charge it. Cost lines and markup are never exposed.","properties":{"total":{"type":"number","description":"Line total in MAD, delivery included at order level"},"totalHT":{"type":"number"},"tvaAmount":{"type":"number"},"tvaRate":{"type":"number"},"discount":{"type":"number","description":"Quantity discount applied, MAD"},"estimatedPrintTime":{"type":"number","description":"Hours of machine time"},"estimatedWeight":{"type":"number","description":"Grams of material"},"estimatedDeliveryDays":{"type":"number"}}}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/quotes/calculate":{"post":{"summary":"Price one file","description":"The price of one line: SKU, finish, quantity, rush. `isRush` is refused with `reason: rushUnavailable` when the print does not fit a working day. Rate limit: 60 per minute per client.","tags":["Quotes"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"fileKey":{"type":"string","minLength":1},"filename":{"type":"string","minLength":1},"materialId":{"type":"string","minLength":1},"finish":{"default":"standard","type":"string","enum":["standard","smooth","polished"]},"quantity":{"default":1,"type":"integer","exclusiveMinimum":0,"maximum":1000},"isRush":{"default":false,"type":"boolean"}},"required":["fileKey","filename","materialId"]}}}},"responses":{"200":{"description":"The line's price.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"analysis":{"type":"object"},"material":{"type":"object","properties":{"id":{"type":"string","description":"The SKU to send as `materialId`"},"name":{"type":"string"},"nameFr":{"type":"string"},"technology":{"type":"string","enum":["fdm"]},"materialType":{"type":"string","description":"Polymer family: pla, petg, abs, asa, tpu, ..."},"pricePerGram":{"type":"number","description":"MAD"},"density":{"type":"number","description":"g/cm³"},"available":{"type":"boolean"},"previewColor":{"type":"string"}}},"quote":{"type":"object","description":"What a part costs, as the checkout will charge it. Cost lines and markup are never exposed.","properties":{"total":{"type":"number","description":"Line total in MAD, delivery included at order level"},"totalHT":{"type":"number"},"tvaAmount":{"type":"number"},"tvaRate":{"type":"number"},"discount":{"type":"number","description":"Quantity discount applied, MAD"},"estimatedPrintTime":{"type":"number","description":"Hours of machine time"},"estimatedWeight":{"type":"number","description":"Grams of material"},"estimatedDeliveryDays":{"type":"number"}}}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/quotes/batch":{"post":{"summary":"Price a basket","description":"The total an order will charge: lines, the per-order service fee, the order minimum, the plate credit and delivery (included). Always price a basket here rather than summing single lines. Rate limit: 60 per minute per client.","tags":["Quotes"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"items":{"minItems":1,"maxItems":25,"type":"array","items":{"type":"object","properties":{"fileKey":{"type":"string","minLength":1},"filename":{"type":"string","minLength":1},"materialId":{"type":"string","minLength":1},"finish":{"default":"standard","type":"string","enum":["standard","smooth","polished"]},"quantity":{"default":1,"type":"integer","exclusiveMinimum":0,"maximum":1000},"isRush":{"default":false,"type":"boolean"}},"required":["fileKey","filename","materialId"]}},"destinationId":{"type":"string","minLength":1}},"required":["items"]}}}},"responses":{"200":{"description":"The basket, priced.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"fileKey":{"type":"string"},"quote":{"type":"object","description":"What a part costs, as the checkout will charge it. Cost lines and markup are never exposed.","properties":{"total":{"type":"number","description":"Line total in MAD, delivery included at order level"},"totalHT":{"type":"number"},"tvaAmount":{"type":"number"},"tvaRate":{"type":"number"},"discount":{"type":"number","description":"Quantity discount applied, MAD"},"estimatedPrintTime":{"type":"number","description":"Hours of machine time"},"estimatedWeight":{"type":"number","description":"Grams of material"},"estimatedDeliveryDays":{"type":"number"}}}}}},"totals":{"type":"object","description":"subtotal, setup, minimumAdjustment, shipping (0: included), total, machineHours"}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/quotes":{"post":{"summary":"Save a devis","description":"Saves the basket as a numbered quote (DV-YYYY-NNNN), valid for a week, re-priced from the files. Rate limit: 20 per minute per client.","tags":["Quotes"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"items":{"minItems":1,"maxItems":25,"type":"array","items":{"type":"object","properties":{"fileKey":{"type":"string","minLength":1},"fileName":{"type":"string","minLength":1},"fileSize":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},"materialId":{"type":"string","minLength":1},"technology":{"type":"string","enum":["fdm"]},"finish":{"default":"standard","type":"string","enum":["standard","smooth","polished"]},"quantity":{"default":1,"type":"integer","minimum":1,"maximum":9007199254740991}},"required":["fileKey","fileName","fileSize","materialId","technology"]}}},"required":["items"]}}}},"responses":{"201":{"description":"Saved.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"id":{"type":"string"},"reference":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"itemCount":{"type":"integer"},"totalPrice":{"type":"number"}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"summary":"List my devis","description":"The caller's saved quotes.","tags":["Quotes"],"parameters":[{"name":"status","in":"query","required":false,"schema":{"type":"string","enum":["draft","pending","confirmed","expired"]}}],"responses":{"200":{"description":"The quotes.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/quotes/{id}":{"get":{"summary":"A devis","description":"With its lines.","tags":["Quotes"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Quote id"}],"responses":{"200":{"description":"The quote.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object"}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Not the caller's quote.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No such quote.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"patch":{"summary":"Update a devis","description":"Its status. Rate limit: 5 per minute per client.","tags":["Quotes"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Quote id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["draft","pending","confirmed","expired"]}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object"}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"summary":"Delete a devis","description":"And its lines.","tags":["Quotes"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Quote id"}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"id":{"type":"string"}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/quotes/{id}/pdf":{"get":{"summary":"The devis as PDF","description":"Re-priced from the files; `application/pdf`.","tags":["Quotes"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Quote id"}],"responses":{"200":{"description":"The PDF.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/addresses":{"get":{"summary":"My addresses","description":"The caller's delivery addresses. Rate limit: 60 per minute per client.","tags":["Orders"],"responses":{"200":{"description":"The addresses.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"summary":"Add an address","description":"`shippingDestinationId` must be an id from /api/shipping/destinations; an unknown city is refused with `reason: cityNotServed`. Rate limit: 20 per minute per client.","tags":["Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":50},"fullName":{"type":"string","minLength":2,"maxLength":100},"phone":{"type":"string","pattern":"^(\\+212|0)(5|6|7)\\d{8}$"},"street":{"type":"string","minLength":5,"maxLength":200},"city":{"type":"string","minLength":2,"maxLength":100},"shippingDestinationId":{"type":"string","minLength":1},"postalCode":{"type":"string","pattern":"^\\d{5}$"},"isDefault":{"default":false,"type":"boolean"}},"required":["label","fullName","phone","street","city","shippingDestinationId"]}}}},"responses":{"201":{"description":"Created.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/addresses/{id}":{"patch":{"summary":"Edit an address","description":"The caller's own. Rate limit: 20 per minute per client.","tags":["Orders"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Address id"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string","minLength":1,"maxLength":50},"fullName":{"type":"string","minLength":2,"maxLength":100},"phone":{"type":"string","pattern":"^(\\+212|0)(5|6|7)\\d{8}$"},"street":{"type":"string","minLength":5,"maxLength":200},"city":{"type":"string","minLength":2,"maxLength":100},"shippingDestinationId":{"type":"string","minLength":1},"postalCode":{"type":"string","pattern":"^\\d{5}$"},"isDefault":{"default":false,"type":"boolean"}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object"}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"summary":"Remove an address","description":"The caller's own. Rate limit: 20 per minute per client.","tags":["Orders"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Address id"}],"responses":{"204":{"description":"Removed."},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/orders":{"post":{"summary":"Place an order","description":"Priced again from the files on the server; the response total is what Stripe will charge. `acceptTerms` must be `true`. The order is created unpaid; pay it through /api/payments/checkout. Rate limit: 20 per minute per client.","tags":["Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"addressId":{"type":"string","minLength":1},"quoteId":{"type":"string","minLength":1},"acceptTerms":{"type":"boolean","const":true},"items":{"minItems":1,"maxItems":25,"type":"array","items":{"type":"object","properties":{"quoteItemId":{"type":"string","minLength":1},"fileKey":{"type":"string","minLength":1},"fileName":{"type":"string","minLength":1},"materialId":{"type":"string","minLength":1},"technology":{"type":"string","enum":["fdm"]},"finish":{"default":"standard","type":"string","enum":["standard","smooth","polished"]},"quantity":{"type":"integer","exclusiveMinimum":0,"maximum":1000},"isRush":{"default":false,"type":"boolean"}},"required":["fileKey","fileName","materialId","technology","quantity"]}}},"required":["addressId","acceptTerms","items"]}}}},"responses":{"201":{"description":"Created, unpaid.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"id":{"type":"string"},"orderNumber":{"type":"string"},"status":{"type":"string"},"subtotalAmount":{"type":"number"},"shippingCost":{"type":"number"},"totalAmount":{"type":"number"},"itemCount":{"type":"integer"}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"summary":"My orders","description":"The caller's orders, newest first.","tags":["Orders"],"responses":{"200":{"description":"The orders.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"array","items":{"type":"object"}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/orders/{id}/invoice":{"get":{"summary":"The invoice PDF","description":"Issued when the order was paid; 404 before.","tags":["Orders"],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Order id"}],"responses":{"200":{"description":"The PDF.","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not paid yet, or not the caller's order.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/payments/checkout":{"post":{"summary":"Pay an order","description":"Returns a Stripe Checkout URL for the order's total, in MAD. The order turns `paid` when Stripe confirms, never from the client. Rate limit: 20 per minute per client.","tags":["Orders"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"orderId":{"type":"string","minLength":1},"locale":{"default":"fr","type":"string","enum":["fr","en"]}},"required":["orderId"]}}}},"responses":{"200":{"description":"Where to send the payer.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"url":{"type":"string","format":"uri"}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/requests":{"post":{"summary":"Submit a custom project","description":"For work the instant quote cannot price: a part to reproduce from photos, a batch nobody has modelled. A human answers with a quote. Rate limit: 5 per minute per client.","tags":["Projects"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"vertical":{"type":"string","enum":["automotive","cosplay","hobby","other"]},"title":{"type":"string","minLength":3,"maxLength":120},"description":{"type":"string","minLength":20,"maxLength":2000},"quantity":{"default":1,"type":"integer","exclusiveMinimum":0,"maximum":1000},"phone":{"type":"string","pattern":"^(\\+212|0)(5|6|7)\\d{8}$"},"budgetRange":{"type":"string","enum":["b1","b2","b3","b4"]},"deadline":{"type":"string","format":"date-time"},"referenceUrl":{"anyOf":[{"type":"string","maxLength":500,"format":"uri"},{"type":"string","const":""}]},"fileKey":{"type":"string","maxLength":500},"materialPref":{"type":"string","maxLength":60},"finishPref":{"type":"string","enum":["standard","smooth","polished"]}},"required":["vertical","title","description"]}}}},"responses":{"201":{"description":"Received.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"id":{"type":"string"},"reference":{"type":"string"},"status":{"type":"string"}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"get":{"summary":"My project requests","description":"The caller's requests.","tags":["Projects"],"responses":{"200":{"description":"The requests.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"requests":{"type":"array","items":{"type":"object"}}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/cards":{"post":{"summary":"Price a business card","description":"Text and style choices only; the mesh is generated and priced on the server. Rate limit: 20 per minute per client.","tags":["Cards"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":40},"title":{"default":"","type":"string","maxLength":40},"company":{"default":"","type":"string","maxLength":40},"phone":{"default":"","type":"string","maxLength":40},"email":{"default":"","type":"string","maxLength":40},"website":{"default":"","type":"string","maxLength":40},"thickness":{"default":"standard","type":"string","enum":["thin","standard","thick"]},"format":{"default":"eu","type":"string","enum":["eu","us"]},"corners":{"default":"square","type":"string","enum":["square","soft","round"]},"arrangement":{"default":"classic","type":"string","enum":["classic","markLeft","stacked"]},"frame":{"default":"none","type":"string","enum":["none","line","double","corners"]},"divider":{"default":"line","type":"string","enum":["line","short","dots","none"]},"icons":{"default":false,"type":"boolean"},"align":{"default":"left","type":"string","enum":["left","center","right"]},"textCase":{"default":"upper","type":"string","enum":["upper","asTyped"]},"typography":{"default":{},"type":"object","propertyNames":{"type":"string","enum":["name","title","company","phone","email","website"]},"additionalProperties":{"type":"object","properties":{"font":{"type":"string","enum":["block","bold","dot","archivo-black","rubik-mono","russo-one","titillium-black","montserrat","poppins","space-grotesk","barlow-condensed","roboto-slab","zilla-slab","bebas-neue","righteous"]},"size":{"type":"string","enum":["small","medium","large"]},"tracking":{"type":"string","enum":["normal","wide","wider"]}}}},"relief":{"default":"emboss","type":"string","enum":["emboss","engrave"]},"reliefDepth":{"default":"standard","type":"string","enum":["shallow","standard","deep"]},"textDepth":{"default":"match","type":"string","enum":["match","shallow","standard","deep"]},"markDepth":{"default":"match","type":"string","enum":["match","shallow","standard","deep"]},"fontFamily":{"default":"block","type":"string","enum":["block","bold","dot","archivo-black","rubik-mono","russo-one","titillium-black","montserrat","poppins","space-grotesk","barlow-condensed","roboto-slab","zilla-slab","bebas-neue","righteous"]},"fontSize":{"default":"medium","type":"string","enum":["small","medium","large"]},"mark":{"default":"initials","type":"string","enum":["none","initials","logo"]},"qr":{"default":false,"type":"boolean"},"markUrl":{"default":"","type":"string","maxLength":120},"logoKey":{"default":"","type":"string","maxLength":200},"textColorId":{"default":"","type":"string","maxLength":64},"markColorId":{"default":"","type":"string","maxLength":64},"placements":{"type":"object","propertyNames":{"type":"string","enum":["name","title","company","phone","email","website","mark","qr"]},"additionalProperties":{"type":"object","properties":{"dx":{"default":0,"type":"number","minimum":-85,"maximum":85},"dy":{"default":0,"type":"number","minimum":-55,"maximum":55},"scale":{"default":1,"type":"number","minimum":0.5,"maximum":2.5}}}},"materialId":{"type":"string","minLength":1},"finish":{"default":"standard","type":"string","enum":["standard","smooth","polished"]},"quantity":{"default":50,"type":"integer","exclusiveMinimum":0,"maximum":1000}},"required":["name","materialId"]}}}},"responses":{"200":{"description":"The card, priced.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"fileKey":{"type":"string"},"quote":{"type":"object","description":"What a part costs, as the checkout will charge it. Cost lines and markup are never exposed.","properties":{"total":{"type":"number","description":"Line total in MAD, delivery included at order level"},"totalHT":{"type":"number"},"tvaAmount":{"type":"number"},"tvaRate":{"type":"number"},"discount":{"type":"number","description":"Quantity discount applied, MAD"},"estimatedPrintTime":{"type":"number","description":"Hours of machine time"},"estimatedWeight":{"type":"number","description":"Grams of material"},"estimatedDeliveryDays":{"type":"number"}}}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/user/profile":{"patch":{"summary":"Update my profile","description":"Name and phone. Rate limit: 20 per minute per client.","tags":["Account"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":100},"phone":{"type":"string","pattern":"^(\\+212|0)(5|6|7)\\d{8}$"},"image":{"type":"string","format":"uri"}}}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object"}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session: sign in first.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/contact":{"post":{"summary":"Write to the workshop","description":"Sends an email to the team. Public. Rate limit: 5 per minute per client.","tags":["Account"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"email":{"type":"string","format":"email","pattern":"^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},"phone":{"type":"string","maxLength":20},"subject":{"type":"string","enum":["quote","order","technical","partnership","other"]},"message":{"type":"string","minLength":10,"maxLength":2000}},"required":["name","email","subject","message"]}}}},"responses":{"200":{"description":"Sent.","content":{"application/json":{"schema":{"type":"object","required":["success","data"],"properties":{"success":{"type":"boolean","const":true},"data":{"type":"object","properties":{"sent":{"type":"boolean"}}}}}}}},"400":{"description":"Validation failed, or a business rule refused the request; `error.details` says which.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited; retry after a minute.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"sessionCookie":{"type":"apiKey","in":"cookie","name":"better-auth.session_token","description":"Set by POST /api/auth/sign-in/email. Send it back as a cookie on every authenticated call."}},"schemas":{"Error":{"type":"object","required":["success","error"],"properties":{"success":{"type":"boolean","const":false},"error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","enum":["VALIDATION_ERROR","INVALID_INPUT","MISSING_FIELD","INVALID_FILE_TYPE","FILE_TOO_LARGE","EMPTY_ITEMS","UNAUTHORIZED","INVALID_CREDENTIALS","SESSION_EXPIRED","FORBIDDEN","INSUFFICIENT_PERMISSIONS","NOT_FOUND","RESOURCE_NOT_FOUND","USER_NOT_FOUND","ORDER_NOT_FOUND","QUOTE_NOT_FOUND","FILE_NOT_FOUND","CONFLICT","DUPLICATE_ENTRY","RATE_LIMITED","INTERNAL_ERROR","DATABASE_ERROR","STORAGE_ERROR","EXTERNAL_SERVICE_ERROR"]},"message":{"type":"string"},"details":{"type":"object","description":"Context for the error: `errors` (field, message) on VALIDATION_ERROR, `reason` codes such as rushUnavailable, cityNotServed, exceedsBuildVolume, meshNotClosed, quoteMismatch, and a `hint` where one helps.","additionalProperties":true}}}}},"CustomerQuote":{"type":"object","description":"What a part costs, as the checkout will charge it. Cost lines and markup are never exposed.","properties":{"total":{"type":"number","description":"Line total in MAD, delivery included at order level"},"totalHT":{"type":"number"},"tvaAmount":{"type":"number"},"tvaRate":{"type":"number"},"discount":{"type":"number","description":"Quantity discount applied, MAD"},"estimatedPrintTime":{"type":"number","description":"Hours of machine time"},"estimatedWeight":{"type":"number","description":"Grams of material"},"estimatedDeliveryDays":{"type":"number"}}},"Material":{"type":"object","properties":{"id":{"type":"string","description":"The SKU to send as `materialId`"},"name":{"type":"string"},"nameFr":{"type":"string"},"technology":{"type":"string","enum":["fdm"]},"materialType":{"type":"string","description":"Polymer family: pla, petg, abs, asa, tpu, ..."},"pricePerGram":{"type":"number","description":"MAD"},"density":{"type":"number","description":"g/cm³"},"available":{"type":"boolean"},"previewColor":{"type":"string"}}}}}}