Dalinea API
v1.0.0 ExternalUnified API for the Dalinea platform. All endpoints require a valid JWT bearer token. Tenant isolation is enforced via the x-tenant-id header extracted from the token.
Getting Started
Welcome to the Dalinea API. This guide covers the basics of making requests and understanding responses.
Base URL
All API requests are made to:
https://api.kloopify.com
Request Format
The API accepts JSON request bodies and returns JSON responses. Set the following headers on every request:
Content-Type: application/json
Authorization: Bearer <your-token>
Response Format
Successful responses return a 2xx status code with a JSON body. Error responses include a message describing the issue:
{
"statusCode": 400,
"message": "Validation failed",
"error": "Bad Request"
}
Pagination
List endpoints support pagination via offset and limit query parameters:
| Parameter | Default | Description |
|---|---|---|
offset |
0 | Number of records to skip |
limit |
100 | Number of records to return (max 100) |
Rate Limiting
API requests are rate-limited per account. If you exceed the limit, you'll receive a 429 Too Many Requests response. Back off and retry after the duration indicated in the Retry-After header.
Authentication
The Dalinea API uses JWT Bearer tokens for authentication. Every request must include a valid token in the Authorization header.
Obtaining a Token
Tokens are issued by our identity provider. Authenticate with your credentials to receive an access token:
POST https://auth.kloopify.com/oauth/token
Content-Type: application/json
{
"client_id": "<your-client-id>",
"client_secret": "<your-client-secret>",
"audience": "https://api.kloopify.com",
"grant_type": "client_credentials"
}
The response includes an access_token and its expiry:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 86400
}
Using the Token
Include the token in the Authorization header of every API request:
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
Token Expiry
Tokens expire after the duration specified in expires_in (in seconds). When a token expires, the API returns 401 Unauthorized. Request a new token and retry.
Scopes
Access is controlled by scopes embedded in the token. If your token lacks the required scope for an endpoint, the API returns 403 Forbidden.
Running a Cost Analysis
This guide walks through submitting a cost analysis job, tracking its progress, and retrieving the results.
Overview
A cost analysis is an asynchronous operation. You submit a request describing a product and its manufacturer, the system processes it in the background, and you poll for the result. Once complete, the result contains IDs you can use to fetch the full cost analysis, bill of materials, product, and manufacturer records.
Step 1: Submit the Job
Create a cost analysis by posting to the pipeline endpoint:
POST /pipeline/cost-analysis
Content-Type: application/json
Authorization: Bearer <your-token>
{
"productName": "EcoBottle 500ml",
"manufacturerName": "GreenPack Industries",
"countryOfOrigin": "DE",
"productContext": "Reusable water bottle made from recycled stainless steel",
"pricesPaid": [
{ "price": 1250, "currency": "USD", "date": "2025-01-15" }
]
}
The only required fields are productName and manufacturerName. Providing additional context improves the accuracy of the analysis:
| Field | Type | Description |
|---|---|---|
productName |
string | Required. Name of the product |
manufacturerName |
string | Required. Name of the manufacturer |
supplierNames |
string[] | Supplier name(s) |
productWebsite |
string | URL for the product page |
manufacturerWebsite |
string | URL for the manufacturer |
productContext |
string | Free-text description of the product |
manufacturerContext |
string | Free-text description of the manufacturer |
supplierContext |
string | Free-text description of the supplier(s) |
purchaserContext |
string | Context about the purchaser |
countryOfOrigin |
string | ISO country code (defaults to US) |
pricesPaid |
object[] | Historical prices — each with price (number, required), currency (string, required), and date (string, optional) |
The response is 201 Created with a job object:
{
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"type": "cost-analysis",
"status": "pending",
"step": null,
"result": null,
"started": null,
"completed": null
}
Save the id — you'll use it to check on the job.
Step 2: Poll for Completion
Check the status of your job by its ID:
GET /pipeline/cost-analysis/{id}
Authorization: Bearer <your-token>
The status field progresses through these values:
| Status | Meaning |
|---|---|
pending |
Job is queued |
in_progress |
Job is actively processing |
completed |
Job finished successfully |
failed |
Job encountered an error |
Poll every 5–10 seconds until status is completed or failed. The step field provides a human-readable description of what the system is currently working on.
When the job completes, the response includes a result object with IDs for all generated records:
{
"id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
"type": "cost-analysis",
"status": "completed",
"step": "done",
"result": {
"costAnalysisId": "ca-uuid-here",
"bomId": "bom-uuid-here",
"productId": "product-uuid-here",
"manufacturerId": "manufacturer-uuid-here"
},
"started": "2025-06-01T12:00:00.000Z",
"completed": "2025-06-01T12:01:30.000Z"
}
If the job fails, errorMessage describes what went wrong.
Step 3: Retrieve the Results
Use the IDs from the result to fetch the generated records.
Cost Analysis
GET /cost-analysis/{costAnalysisId}
Authorization: Bearer <your-token>
Returns the cost analysis record including the shouldCostAmount and shouldCostStdErr (both in minor currency units, e.g. cents), the currency, and the analysisDate.
Bill of Materials
GET /bom/{bomId}
Authorization: Bearer <your-token>
Returns the bill of materials with a components list. Each component includes its name, type, cost, quantity, unitType, and countryOfOrigin.
To retrieve the full component details separately:
GET /bom/{bomId}/components
Authorization: Bearer <your-token>
Product and Manufacturer
The productId and manufacturerId can be used to retrieve the product and manufacturer (company) records:
GET /product/{productId}
GET /company/{manufacturerId}
Error Handling
- 404 Not Found — The job or record ID does not exist.
- 401 Unauthorized — Token is missing or expired. See the Authentication guide.
status: "failed"— The job did not complete. CheckerrorMessageon the job for details and resubmit if appropriate.
Bill of Materials
Retrieve bills of materials and their component breakdowns.
/bom/{id}
Get bom by ID
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required | |
| related | query | string[] | optional | Include related entities. Omit for all, "false" for none, or specify field names (repeatable, supports dot notation) |
Responses
| Field | Type | Description |
|---|---|---|
| data | BomDto | |
| id* | string | Unique identifier for the bill of materials record |
| versionLabel* | string | Per-lineage version label (MAJOR.MINOR), e.g., `v1.0`. MAJOR bumps on structural change (new AI generation, scenario fork, scenario promotion); MINOR bumps on revision (DLN-151). Assigned by the orchestrator at write time based on the most-recent prior BOM in the same `(productId, scenarioId)` lineage. |
| sourceType* | string | How this BOM came to exist. `ai_generation` is the initial BOM produced by the AI Step 3 Create-path (DLN-135). `ai_revision` is a diff applied by DLN-151. `import` is loaded from an external source. `human` is manually created. `scenario_fork` branches off an actual BOM into a scenario lineage (DLN-159). `scenario_promotion` is a scenario BOM promoted into actual lineage (DLN-280). The vocabulary is system-owned and small, so an enum constraint is appropriate (contrast `BOM_COMPONENT.type`, which is relaxed to allow orthogonal flag semantics). enum: ai_generation, ai_revision, import, human, scenario_fork, scenario_promotion |
| scenarioId | string | Lineage discriminator. NULL signals the actual lineage; a non-NULL UUID identifies a scenario lineage. The Scenario entity is introduced by DLN-159; until then this is a plain UUID column (`scenario_id`) with no FK constraint. DLN-135 always writes NULL. |
| promotedFromBomId | string | When `sourceType = scenario_promotion`, the scenario BOM that was promoted into actual lineage. NULL otherwise. Plain UUID column (`promoted_from_bom_id`) with no FK constraint — same convention as `priorBomId` and the legacy `parentBomId`. Populated by DLN-280. |
| generationDate* | string | Date and time when the BOM was generated |
| status | string | Status of the BOM generation process (e.g., "pending", "completed", "failed") enum: pending, completed, failed |
| components | BomComponentDto[] | List of components and their costs that make up the product - accepts array of full objects, id strings, or id numbers |
| id* | string | Unique identifier for the BOM component |
| priorBomComponentId | string | Optional predecessor BOM_COMPONENT row in the same temporal lineage. Set by revision flows (DLN-151) when this component evolved from a prior version. NULL for components on a newly generated BOM. Stored as a plain UUID column (`prior_bom_component_id`) with no FK constraint — same convention as `productId` / the existing `parentBomId` on BOM. Lineage integrity is enforced at the application layer. |
| bom | any | The parent BOM - accepts full object, id string, or id number |
| name* | string | Name of the component in the bill of materials |
| type* | string | Materiality classification for the component. One of: `sub-assembly` (has a populated `product` object and multi-entry `materials[]`), `direct-material` (raw input consumed into the finished product; no `product` object), `indirect-material` (packaging, consumable, or waste; paired with exactly one of `isPackaging` / `isConsumable` / `isWaste` set to true). Type and the `isPackaging`/`isConsumable`/`isWaste`/`isByproduct` flags are orthogonal axes: type answers materiality, flags answer cost treatment. DLN-286: enum-enforced at schema + DTO. The cost-analysis job normalizes the legacy value (`raw material`) before write on both create and revision paths; existing rows with non-canonical values stay readable (the DB CHECK add is warn-only via cruddy_constraint_warnings) but must be normalized before update. enum: sub-assembly, direct-material, indirect-material |
| classification | string | DLN-286 (FR-R1): refresh classification — how the Data Refresh prices this node and (Phase 2) whether it recurses. Derived by the cost-analysis job from `type` + `makeVsBuy` and persisted: direct/indirect-material → `commodity`; sub-assembly + MAKE → `manufactured-sub-assembly`; sub-assembly + BUY/UNKNOWN/absent → `bought-sub-assembly` (conservative default — UNKNOWN never opts a node into recursion). A stored value is never overwritten by re-derivation. enum: commodity, manufactured-sub-assembly, bought-sub-assembly |
| catalogEntityId | string | FK to CATALOG_COMMODITY.id or CATALOG_SUB_ASSEMBLY.id (see catalogEntityType). Plain uuid, not a validated schema relation — deferred-FK convention (matches rateSnapshotId on COST_ANALYSIS_COMPONENT_DRIVER): the two catalog tables are a discriminated union, so no single relation target applies. Set by the refresh pipeline (DLN-289) on first successful identity-match; reused on subsequent refreshes so a component never re-resolves identity every run. |
| catalogEntityType | string | Which catalog table catalogEntityId points into, when set. enum: catalog-commodity, catalog-sub-assembly |
| cost* | number | Extended line total for this component in minor currency units (e.g., cents) — quantity is already applied; never multiply by quantity again. Per-unit prices live in estimatedCostPerUnit / referenceCostPerUnit / supplierCostPerUnit. |
| presented | any | The line cost exactly as a user typed it in a reporting currency other than USD, kept beside `cost` (that amount converted to USD at the analysis's fxRate) so it reads back as typed. Written only by a component correction; absent on every other line, which a reader presents at the analysis's rate. A reader uses it only while `currency` is the analysis's reportingCurrency and the amount still converts to `cost`, so a later change to `cost` that carried no new twin falls back to the rate. |
| quantity* | number | Total quantity of this component consumed for the BOM's whole reference batch (refQtyValue of refQtyUnit), on the same basis as `cost`, so cost ≈ estimatedCostPerUnit × quantity. Not a per-unit-of-output figure (DLN-945). Fractional by design (DLN-934): a component pro-rated across the batch, such as a drum, IBC or sack larger than the batch, genuinely has a quantity below 1. Typed `integer`, such a value was floored on the way in, sometimes to 0, while `cost` stayed correct, so the line contradicted itself. Matches the raw-material entry, which has always been `number`. |
| unitType* | string | Unit of measurement for the component (e.g., piece, kilogram, liter) |
| estimatedGramsPerUnit | number | Estimated grams of this component per unit. |
| estimatedCostPerUnit | number | Estimated cost in minor currency units (e.g., cents) of this component per unit. |
| estimatedCostPerUnitStdErr | number | Approximation of the standard error of the estimated cost in minor currency units (e.g., cents) of this component per unit. |
| referenceCostPerUnit | number | DLN-377: universal list / spot / OEM reference price per unit, in minor currency units (cents) — the undiscounted commodity price BOM creation produces. The auditable baseline; never overwritten by downstream adjustments. |
| supplierCostPerUnit | number | DLN-377: the price this manufacturer/supplier actually faces per unit, in minor currency units (cents). Equals referenceCostPerUnit until a supplier-specific pricing mechanism computes it (factor-1 default). |
| supplierDiscountFactor | number | Scale-based discount factor applied to referenceCostPerUnit to produce supplierCostPerUnit (0 < factor <= 1; 1 = no discount). Computed by the supplier purchasing-power step from the buyer's annual commodity / category spend through a capped, saturating curve. Null when no discount has been computed. |
| supplierDiscountBasis | string | Spend basis behind the factor: commodity_scale for raw materials (buyer's spend on the commodity itself), supplier_leverage for bought whole units (buyer's spend on the part category; lower ceiling — only margin and conversion are negotiable). enum: commodity_scale, supplier_leverage |
| supplierDiscountSource | string | Provenance of the financials scale signal the factor was derived from, from filed statements down to structural proxies. enum: edgar_financials, reported_financials, ai_estimate, employees_proxy, supplier_tier, naics_default |
| supplierDiscountConfidence | string | Confidence in the discount, combining the scale-signal quality and the commodity-share source (AI-estimated shares cap at medium). enum: high, medium, low |
| referencePriceSource | string | Which number referenceCostPerUnit actually holds. `ai_estimate` is the estimate BOM creation produced. `customs_anchor` is the customs-derived import unit value published by the price-reference feed. `catalog_price` is a price sourced outside both: the price the catalog carries for this component's matched entity, which a price refresh either wrote from its own market lookup or preserved because an account or an external database had set it. The label names the price that is served — the pipeline records `customs_anchor` only where referenceCostPerUnit is the anchor held in referencePriceComparison. Absent means the reference has not been priced by a pass that records provenance, which is a different fact from `ai_estimate`; nothing backfills a label onto a component no pricing pass has seen. This names the number served, NOT how the component was priced — referencePriceMethod answers that, and the two come apart whenever a refresh replaces the served number without retracting the pricing decision behind it. enum: ai_estimate, customs_anchor, catalog_price |
| referencePriceMethod | string | How this component's reference price was arrived at. `anchor` means the customs-derived import unit value priced it, which is the ordinary case wherever an eligible anchor exists. `ai_fallback` means no eligible anchor was found and the AI estimate priced it instead. The estimate is computed either way and serves as a cross-check, so a disagreement lowers referencePriceConfidence and is recorded in referencePriceReconciliation rather than changing the number. Distinct from referencePriceSource, which names the number actually served. Four pairings are legal and two are refused: a component priced from the anchor cannot serve the estimate, and one that fell back to the estimate cannot serve the anchor. A refresh that serves the anchor records `customs_anchor` as the source and `anchor` here. A refresh that replaces the served price from the catalog records `catalog_price` as the source and leaves this field alone, because how the pipeline priced the component is not retracted by a later price arriving from somewhere else. Absent means no pricing pass has decided for this component, which is a distinct fact from either value. `ai_fallback` appears on legacy rows only: the pipeline no longer writes a label on a component that fell back, so a current fallback leaves this field absent. enum: anchor, ai_fallback |
| referencePriceReconciliation | string | What the estimate-against-anchor cross-check concluded. The first three are verdicts on an anchor that priced the component and had an estimate beside it: corroborated, or off by a minor or a major margin. A major mismatch routes the component to review; neither mismatch changes the price. `no_cross_check` is an anchor that priced the component with no usable estimate standing against it — unreconciled, not disagreeing. `ai_fallback` is the case where no anchor priced it at all. The vocabulary partitions referencePriceMethod rather than crossing it: the first four occur only with method `anchor`, and `ai_fallback` occurs only with method `ai_fallback`. Stored as its own column rather than derived so a reader — and the warehouse — can count outcomes without joining to the comparison; the agreement with the method is enforced on write, because a field that can drift will. Absent means no cross-check pass has run. `ai_fallback` appears on legacy rows only; the pipeline no longer writes it. enum: anchor_corroborated, anchor_mismatch_minor, anchor_mismatch_major, no_cross_check, ai_fallback |
| referencePriceOriginCountry | string | Component countryOfOrigin (ISO alpha-2) at pricing time. Together with referencePriceDestinationCountry this snapshots the requested country context, including on AI fallback, where it is the only reference-price field written: it records where the pass looked and claims no price. An unmapped origin is omitted from the resolver call; referencePriceMatchedScope records what actually matched. |
| referencePriceDestinationCountry | string | Analysis importCountry (ISO alpha-2, US by default) at pricing time. The resolver receives its alpha-3 equivalent. An unmapped destination falls back to AI instead of silently selecting another market. |
| referencePriceMatchedScope | string | Which served price priced this component: the exact origin lane, the destination market row, or the worldwide HS6 row. Written when a customs price serves, and required then. `none` is the lookup's own answer for a code it served nothing for; the pipeline stores it beside the country context on a component that fell back for that reason. It claims no price, so clearing a stale label leaves it in place. Absent where no lookup answered, or where its row could not price the component. enum: lane, market, default_hs6, none |
| referencePriceEditionId | string | The price-reference edition consulted for this component, named by the feed's own edition identifier. Editions are retained, so this is what lets a stored analysis be replayed against the prices it saw. Set only where a customs price served this component; a lookup that served nothing writes no edition, and the per-run coverage log carries that outcome instead. The extraction timestamp is not copied here — it is a property of the edition this identifier already names. Bounded because the field reaches the API as a client-supplied string like any other: an edition id the producer mints runs to a few dozen characters, and nothing about the column stops an arbitrarily large one being stored under its name. |
| referencePriceConfidence | number | How much to trust referenceCostPerUnit, 0 (low) to 100 (high), scored from the anchor's own quality and from how well the estimate corroborates it. Distinct from confidence.cost, which is the estimator's confidence in its own number: this score is evidence from outside it, so a price no anchor covers can carry a low score with no comparison stored at all. Product states one band boundary: High is 75 and above. The Medium/Low split is the scoring pass's to set and is not yet stated, so no boundary for it is published here. A component priced by the AI fallback scores below High by construction. Whole units of a hundred rather than a fraction, so the stored value has one exact representation and sorts and aggregates in the warehouse without a float comparison. |
| referencePriceScoredAgainst | number | The referenceCostPerUnit that referencePriceConfidence was taken against, in minor currency units (cents). The served reference can move after a score is written — a price refresh replaces it, and a judge correction re-aligns it to a corrected estimate — and a score that outlives its subject reads as current when it is not. Holding the number scored makes that visible: where this differs from referenceCostPerUnit, the score describes a price the component no longer serves. |
| referencePriceComparison | any | What the customs anchor said about this component's reference price, and the arithmetic behind the score. Held so a reviewer can redo the comparison from the row alone instead of re-reading the edition, and so the record of anchor-versus-estimate accumulates while the estimate is still the number served. Absent where the feed covered nothing for this component, or was not consulted. |
| referencePriceChoice | any | Which of the customs anchor (the basket price) and the AI estimate this line serves, why, and how far apart they are. A refresh records it too, once the round has priced the line. The anchor serves only a bulk-grade material line, and only while it is between half and double the estimate, bounds included; every other line keeps the estimate. Written by the pricing pass on every line where an anchor could be brought to the line's unit, whatever the line's grade, so both prices are kept whichever one serves. Stored beside referencePriceReconciliation rather than replacing it: that flag is the cross-check's verdict on a served anchor, this is the choice between the two numbers. Internal evidence; no screen shows it. Written by the last pass that weighed an anchor, and never left stale: a judge correction that moves the price clears it, and so does a later pass that finds no usable anchor or a price set by hand. An anchor an earlier pass served that the rule now refuses goes back to the estimate, and the record says why. Absent where no anchor could price the line, where a price set by hand outranked it, or where the line's quantity held its prices. |
| countryOfOrigin | string | Country where the component is most likely manufactured (two digit ISO country code, e.g., US, CN). |
| makeVsBuy | string | Whether the product's manufacturer makes this component in-house (`MAKE`) or buys it from an upstream supplier (`BUY`). `UNKNOWN` when it cannot be determined — do not guess. Classification only: this is an explicit, auditable input signal and drives no cost adjustment here. The implicit default mapping is sub-assembly→`BUY`, direct-material→`MAKE`; orthogonal to `type`. enum: MAKE, BUY, UNKNOWN |
| buyDetails | any | Upstream-sourcing detail for `BUY` components. Omit for `MAKE`/`UNKNOWN`. The supplier here is who the part is purchased from, which is distinct from the sub-assembly `product.manufacturer` (who made it) even when they coincide. |
| htsusCode | string | The Harmonized Tariff Schedule of the United States line this component classifies to — the component's own code, distinct from the nested `product.htsusCode`, which describes a referenced product identity and exists only on sub-assemblies. Written as the schedule publishes it: dotted, ten digits for a statistical line and eight for a subheading the schedule gives no statistical breakout. The pattern is the same one `product.identifiers.htsusCode` carries, and it is the write boundary: the first six digits are the subheading commodity price references are keyed on, and a value that is not a dotted code would still yield six leading characters — a join key made of something that is not a code. Null means no code. The three HTS fields form one state at every write boundary: a code requires a non-`unresolved` `htsusSource`; `unresolved` forbids both code and taxonomy version; clearing a code clears the other two columns rather than leaving stale provenance behind. |
| htsusSource | string | Where `htsusCode` came from, and — when there is none — why. This is the discriminator for the component's tariff state. `deterministic`: picked from the classification corpus; the BOM service still confirms the exact line in the published schedule (usitc_hts_10digit) before accepting it. `ai_estimate`: free-hand model output — the same token `supplierDiscountSource` uses on this row for the same kind of value. `user`: a person's explicit pick, which outranks re-derivation. `unresolved`: classification ran and produced nothing the schedule confirms — a gap, and therefore a no-code state. Absent means classification has not run for this component. A user or AI pin the reference does not publish may keep its code for review but has no `htsusTaxonomyVersion` and exposes no commodity-price key. enum: deterministic, ai_estimate, user, unresolved |
| htsusTaxonomyVersion | string | Server-owned revision of the published schedule the code was confirmed against — the `version` the exact matching `usitc_hts_10digit` row carries, e.g. `2026HTSRev10`. A caller may echo a value it read, but the BOM service accepts it only when a fresh reference lookup returns that same revision and always writes the lookup result. Recorded so a code stays reproducible across a schedule revision: the same component reclassified after a revision can legitimately land elsewhere, and without the revision the two answers are indistinguishable from a defect. Null when `htsusCode` is null or the exact line is unpublished; changing or clearing a code clears the old revision before any new one is stamped. |
| beaCode | string | BEA IO commodity code identifying what this component is, as a commodity — the lookup key into the BEA×NAICS composition matrix (bea_composition dataset). Chosen from the controlled bea_commodities vocabulary at BOM generation and enforced against it before persistence: a code outside the vocabulary is nulled with beaUnmappedReason set. Null means unmapped; downstream composition lookups fall back to estimates. Sub-assemblies carry the commodity category of the purchased article, not their dominant raw material. |
| beaConfidence | string | Model-reported confidence in the beaCode mapping. Null when the component is unmapped. enum: high, medium, low |
| beaUnmappedReason | string | Why beaCode is null — either the model found no vocabulary entry that covers the component, or a proposed code was rejected as outside the vocabulary. Null when beaCode is set. |
| isPackaging | boolean | Indicates if the component is packaging material used to contain, protect, or present the finished product (e.g., cartons, labels, shrink wrap). Packaging contributes to Indirect Materials with a positive cost. |
| isWaste | boolean | Indicates if the component is scrap or non-valuable residue generated during production that must be disposed of. Waste has its own cost category (positive price and cost). If a byproduct would require payment to dispose of, classify it as waste, not a byproduct. |
| isConsumable | boolean | Indicates if the component is an indirect process consumable used during manufacturing that does not end up in the final product (e.g., sterilization agents, cleaning solvents, process gases, water). Consumables contribute to Indirect Materials with a positive cost. |
| isByproduct | boolean | Indicates if the component is a valuable co-product of the manufacturing process that generates revenue or offsets cost (e.g., a chemical intermediate sold to another industry). Byproducts carry a positive price but a negative cost representing the credit received. Direct Materials stays gross; the credits come off the analysis's total as its Byproduct Credits bucket. If the component instead requires payment to dispose of, classify it as isWaste, not isByproduct. |
| materialGrade | string | The grade of material this line buys, set on every direct-material and indirect-material line. `bulk`: sold as a commodity — resins, metals, basic chemicals, paper, standard packaging. `special`: pharma, medical, high-purity, electronic, reagent or enzyme grades. `not_a_purchase`: scrap, waste, by-products, anything made on site; a line flagged isWaste or isByproduct is always `not_a_purchase`. Null on sub-assemblies, which are bought parts, and on lines from analyses made before the grade was recorded. A line with no grade never takes a basket price. A revision keeps a line's grade unless the line itself changes. enum: bulk, special, not_a_purchase |
| sources | string[] | List of URIs to sources or references for the component information. This can be spec sheets, manufacturer websites, datasheets, etc. |
| confidence | any | Detailed confidence information about various aspects of the component data |
| justification | string | Explanation of the reasoning behind all of the mappings made in this component (e.g., sources, justification for assumptions, confidence, etc.). Keep this to less than 5000 characters. Can also include other considerations about this product. |
| materials | RawMaterialEntryDto[] | Estimated raw materials that make up this component. In the case that this component is just a single @raw-material-entry, this is just the one raw material itself in the array. In the case of a sub-assembly, this is the list of @raw-material-entries that make up the sub-assembly. - accepts array of full objects, id strings, or id numbers |
| productId | string | Optional reference to PRODUCT when this component represents a known sub-product. Cross-service reference — Cruddy does not emit a FK constraint (DLN-168 forbids cross-service FKs). Populated by recursive sub-product resolution (DLN-154); NULL otherwise. |
| product | any | Information about the @product that the component is, when the component is a sub-assembly. Every component of type "sub-assembly" MUST populate this object; omit it only when the component is a raw material. If the manufacturer of the sub-assembly is unknown or the sub-assembly is a commodity, still populate the object and set the manufacturer to GENERIC — an unidentified manufacturer is not a reason to leave the object out, nor to reclassify a genuine sub-assembly as a raw material. |
| refQtyValue | number | Reference quantity of output product this BOM represents. Examples: 1 for finished goods (paired with `refQtyUnit: "piece"`); 1, 100, 1000 for chemicals/commodities (paired with `refQtyUnit: "kilogram"` or similar). Per-component `cost`/`quantity` values are calibrated against this reference batch. |
| refQtyUnit | string | Unit of measurement for `refQtyValue` (e.g., `kilogram`, `piece`, `liter`). |
| assumptions | string[] | Critical assumptions about process, material grade, formulation, yield, or other factors that influenced the BOM composition and component costs. Examples: assumed reaction route, catalyst choice, solvent grade, food-grade vs industrial-grade materials, assembly method, raw material form factor. |
| processParams | any | Manufacturing process classification for cost modeling. Used to apply yield, compliance, and setup adjustments in cost analysis. |
| productId | string | ID of the product this BOM was generated for. Enables direct querying of all BOMs for a given product. |
| priorBomId | string | Predecessor BOM in the same `(productId, scenarioId)` lineage. NULL on the first BOM in a lineage; set on every successor (generation, revision, scenario fork, scenario promotion). Establishes per-lineage chaining. Plain UUID column (`prior_bom_id`) with no FK constraint — same convention as the legacy `parentBomId` field this replaces. |
| revisionDiff | object | The structured diff that was applied to the parent BOM to produce this revision. Stored for auditability and lineage tracking. Contains operations (modify/add/remove), updatedAssumptions, and a summary of changes. Stored as opaque JSON. |
| meta | object |
Company
Look up company profiles and related information.
/company/{id}
Get company by ID
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required | |
| related | query | string[] | optional | Include related entities. Omit for all, "false" for none, or specify field names (repeatable, supports dot notation) |
Responses
| Field | Type | Description |
|---|---|---|
| data | CompanyDto | |
| id | string | Unique identifier for the company record |
| name* | string | Legal or commonly known name of the company |
| nameNormalized | string | Deterministic normalization of `name` used for per-tenant deduplication (lowercase + trim + collapse-whitespace + strip-punctuation + strip common corporate suffixes + Unicode NFKC). Service-computed on write; not client-supplied. Indexed per-tenant by a partial unique index installed at service bootstrap. See DLN-133. |
| accountId | string | FK to the account that owns this company record. When null, the company is considered a public/shared record. |
| legalStructure | string | Legal entity type (e.g., LLC, Inc, GmbH, Pty Ltd) |
| identifiers | any | Official business registration identifiers as key-value pairs. Values are the corresponding registration numbers. The well-known keys are declared below; other identifier types are still accepted. |
| country | string | Country of incorporation (ISO 3166-1 alpha-2 code, e.g., US, CA, GB, DE) |
| naicsIndustries | string[] | Two-digit NAICS sector codes describing the company's primary industries |
| naics | string[] | Six-digit NAICS codes describing the company's specific lines of business |
| founded | string | Date the company was founded or incorporated |
| domains | string[] | Company domain names (e.g., google.com, abc.xyz) |
| website | string | Primary corporate website URL |
| status | string | Current operational status of the company enum: active, dissolved, merged, acquired |
| headquarters | any | Primary headquarters address - accepts full object, id string, or id number |
| employees | number | Approximate total number of employees |
| aliases | string[] | Alternative or former names the company is known by (e.g., trade names, DBAs, previous names) |
| logos | LogosDto[] | Company logos, past and present, with usage dates |
| logo* | string | URI to the logo image file (full GCS path) |
| status | string | Whether this logo is currently in use enum: active, inactive |
| description | string | Label or context for this logo (e.g., primary, icon, wordmark) |
| start_date | string | Date this logo started being used |
| end_date | string | Date this logo stopped being used, if inactive |
| socials | SocialsDto[] | Social media profiles associated with the company |
| type* | string | Social media platform (e.g., linkedin, twitter, facebook) |
| url | string | Full URL to the company's profile on this platform |
| username | string | Company's username or handle on this platform |
| addresses | AddressesDto[] | Additional company locations beyond the headquarters (e.g., offices, branches, factories) |
| type* | string | Location type (e.g., office, branch, factory, warehouse) |
| address* | any | Physical address of this location - accepts full object, id string, or id number |
| ticker | string | Stock ticker symbol for a publicly-traded company (e.g., AAPL, BRK.A). Used to resolve the company's SEC CIK for EDGAR financial-statement lookups. |
| cik | string | SEC Central Index Key, 10-digit zero-padded. Populated on first successful EDGAR resolution for a US public filer. |
| financials | CompanyFinancialsDto[] | Annual financial snapshots (revenue, COGS, SGA components) for this company, one per fiscal year. Sourced from EDGAR filings or operator entry. See DLN-73. - accepts array of full objects, id strings, or id numbers |
| id | string | Unique identifier for the financials record |
| company | any | The parent company this snapshot describes - accepts full object, id string, or id number |
| fiscalYear* | number | Fiscal year covered by this snapshot |
| periodStart* | string | Start date of the reporting period (e.g., 2024-01-01) |
| periodEnd* | string | End date of the reporting period. Used by the cross-taxonomy validity guard to reject stale observations where values lag revenue's period by more than ~1 year. |
| statementType* | string | Filing type the snapshot was extracted from, or "manual" when entered by an operator. enum: 10-K, 20-F, manual |
| currency* | string | ISO 4217 currency code (e.g., USD, JPY) for all numeric amounts in this row. Values are stored as-reported in this currency. |
| revenue* | number | Annual revenue, as reported in the filing, in units of `currency`. |
| cogs | number | Annual cost of goods sold, as reported in the filing, in units of `currency`. Null when the filing yields no COGS figure or an operator did not know it. Never 0 for "unknown". |
| salesAndMarketing | number | Sales & Marketing expense component of "our SGA" (sum of S&M + G&A + R&D, deliberately excluding non-operational items). Nullable when the filer does not break it out. |
| generalAndAdministrative | number | General & Administrative expense component of "our SGA". Nullable when the filer does not break it out. |
| researchAndDevelopment | number | Research & Development expense component of "our SGA". Nullable when the filer does not break it out. |
| sellingGeneralAndAdministrative | number | The combined "Selling, general and administrative" line as filed (us-gaap:SellingGeneralAndAdministrativeExpense), for filers that report one line instead of separate S&M and G&A components. Null when the filer breaks the components out, or files neither. Kept distinct from the components so a consumer can tell a lumped figure from a summed one (DLN-884). |
| operatingIncome | number | Operating income as reported (us-gaap:OperatingIncomeLoss). Signed — a loss is negative, unlike every other figure on this row. Recorded as drift-detection evidence only: the cost-analysis pipeline derives its own operating margin from revenue, COGS and SG&A and does not read this figure (DLN-913), because we cannot know what the filer included when they calculated it. Nullable. |
| filingDate | string | SEC filing/accepted date of the underlying 10-K or 20-F. Drives the 300-day EDGAR refresh check. Null for manual rows. |
| accessionNumber | string | EDGAR accession number (e.g., 0000008670-24-000011) that sourced this row, for audit and per-run traceability. Null for manual rows. |
| retrievedAt | string | Timestamp at which this row was written (EDGAR fetch or manual entry). |
| lastCheckedAt | string | Last time EDGAR was probed for this row. Combined with a 30-day negative-cache window to prevent thrashing when filingDate has passed the 300-day fresh window but no newer filing exists. |
| source* | string | Provenance of this row. Only "edgar" and "manual" rows are persisted. LLM fallback responses are never persisted (returned directly to caller with source="llm" in the response envelope). enum: edgar, manual |
| enteredBy | string | Operator who entered this row. Required when source="manual"; null for "edgar". |
| reason | string | Operator-supplied justification for a manual entry (e.g., "EDGAR extraction failed; numbers from investor presentation"). Required when source="manual". |
| basis | string | Whose figures these are, as a value to branch on rather than a sentence to parse. "own" - the company was bound to its own SEC registrant. "parentConsolidated" - it was bound through a subsidiary relationship, so revenue and costs are the PARENT registrant's consolidated filings and describe a larger entity than this company; treat them as qualified rather than exact. Null means no basis is claimed, not that the basis is "own": manual rows carry figures an operator typed in, for which this service asserts nothing, and an EDGAR row can also arrive here when the evidence needed to decide was unavailable. Read `source` to tell those apart - a null basis on source="manual" means no claim was made, and on source="edgar" it means the evidence needed to decide could not be read. enum: own, parentConsolidated |
| basisNote | string | Human-readable companion to `basis`, naming the parent registrant when basis="parentConsolidated". Display text only - branch on `basis` and read the parent off `basisRegistrantCik` / `basisRegistrantName`, never off this string. Null when there is nothing to qualify. |
| basisRegistrantCik | string | The SEC registrant whose filings these figures came from, when basis="parentConsolidated" - the machine-readable form of the entity `basisNote` names in prose. Both are built from the same resolution, so they cannot disagree. Null unless there is a parent to name. |
| basisRegistrantName | string | The registrant title behind `basisRegistrantCik`, with EDGAR's disambiguation markers stripped ("INTERNATIONAL PAPER CO", not "INTERNATIONAL PAPER CO /NEW/"). Display or match on it; the CIK is the identifier. Null unless there is a parent to name. |
| identityRoute | string | Which evidence selected the SEC registrant these figures were read from, as a value to branch on. `basis` answers whose figures these are, and deliberately does not answer this: a bind can be the company's own registrant on every reading and still have been SELECTED by weaker evidence than the row beside it. "suppliedIdentifier" - the CIK or ticker already on the company record, set by an operator or persisted by an earlier resolution. "nameMatch" - the company's own name is EQUAL to exactly one registrant's current or former title once both are normalized. "alias" - a row in the alias table vouched for the CIK (an EDGAR former-name or Exhibit 21 record, or an operator assertion). "researchedTicker" - a researched ticker symbol selected the registrant and that registrant's own name corroborated it, but nothing stronger did; the weakest route, and the only one whose CIK is deliberately never written back to the company record, so it is re-earned on every fetch rather than frozen. Null on manual rows, which no resolution bound, and on rows written before this field existed - not a synonym for any route. `reason` carries the prose companion on a "researchedTicker" row. enum: suppliedIdentifier, nameMatch, alias, researchedTicker |
| commodityDiscounts | CompanyCommodityDiscountDto[] | Cached supplier purchasing-power discount factors for this company, one per (commodityCategory, fiscalYear). Written by the cost-analysis supplier-power step; a reusable derived value, recomputed when the underlying financials change. - accepts array of full objects, id strings, or id numbers |
| id | string | Unique identifier for the cached factor row |
| company | any | The company whose purchasing power the factor describes - accepts full object, id string, or id number |
| commodityCategory* | string | Commodity the factor applies to — the BEA IO commodity code carried on the discounted BOM components (the composition-matrix lookup key). |
| fiscalYear* | number | Fiscal year of the financials the factor was derived from. |
| factor* | number | Discount factor (0 < factor <= 1; 1 = no discount) applied to a reference price to produce the supplier-facing price. |
| basis* | string | Spend basis the factor was computed on. enum: commodity_scale, supplier_leverage |
| source* | string | Provenance of the financials scale signal. enum: edgar_financials, reported_financials, ai_estimate, employees_proxy, supplier_tier, naics_default |
| confidence* | string | Confidence in the factor. enum: high, medium, low |
| spendUsd | number | The annual spend estimate (USD) that produced the factor, kept for auditability of the curve input. |
| computedAt | string | When the factor was last computed. |
| metadata | object | Additional metadata about the company as key-value pairs |
| meta | object |
Product
Look up product profiles and related information.
/product/{id}
Get product by ID
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required | |
| related | query | string[] | optional | Include related entities. Omit for all, "false" for none, or specify field names (repeatable, supports dot notation) |
Responses
| Field | Type | Description |
|---|---|---|
| data | ProductDto1 | |
| id* | string | Unique identifier for the product |
| name* | string | Name of the product |
| description | string | Detailed description of the product |
| retailPrice | number | Approximate, current price of the product, in minor currency units (e.g., cents) |
| retailPriceCurrency | string | Currency code for the retail price (e.g., USD, EUR) |
| unitType | string | Unit of measurement for the @product (e.g., piece, kilogram, liter) enum: piece, kilogram, gram, liter, milliliter, meter, foot, square meter, square foot, cubic meter, gallon, pound, ounce, ton, sheet, roll, barrel, board foot |
| status | string | Lifecycle status of the product enum: in development, active, end-of-life |
| commoditized | boolean | Indicates if the product is classified as a commodity - very little differentiation between competitors |
| rawMaterial | boolean | Indicates if the product is considered a raw material used in manufacturing or production (e.g., steel, plastic, lumber) |
| parentProductId | string | FK to the parent @product, if applicable |
| categoryId | string | DEPRECATED (DLN-793), read-only compatibility for associations stored before category classification moved to identifiers.unspscCode. New writes and categoryId filters are rejected. Keep this field readable until scripts/dln-793-audit-product-category-removal.sql has passed in every live environment and an explicit follow-up migration removes it. |
| countryOfOrigin | string | Country where the product is manufactured (ISO 3166 alpha-2 code, e.g., US, CN) |
| weightGrams | number | Product weight in grams |
| leadTimeDays | number | Typical lead time in days |
| minimumOrderQuantity | number | Minimum order quantity |
| negotiationUnit | any | The unit the buyer negotiates in and IMPACT analyses (ADR 0156): a name, such as "pack of 12" or "10 kg tote", and how many base units it holds. The base unit is the one the product's transactions are normalised to (its roll-up's baseUnit). Absent means one base unit. IMPACT's price, should-cost, gap and quantity are all expressed in it; SIGNAL does not use it. Send null to go back to one base unit. |
| metadata | object | Additional account-specific metadata about the product |
| identifiers | any | Various identifiers associated with the product |
| urls | any | Various URLs associated with the product |
| productKey | string | For a product made by grouping a transaction import's lines (ADR 0150): the product key its lines share, sha256 hex (ADR 0145). A later import's lines with the same key attach to this product. Null on a product made any other way, and on a NOT_GROUPED one, which no line can join. One live product per key per organisation, by a unique index installed at bootstrap. Set by the server, never by a request. |
| productKeyVersion | string | The version of the product key productKey was computed under. Null when productKey is. |
| productKeyValues | any | The normalized values that define a grouped product, as the product key computed them: what its lines have in common. Null when productKey is, except on a product split out of a group in review (ADR 0157), which keeps the values of the group it came from. |
| groupingRung | string | For a product made by grouping a transaction import's lines (ADR 0150): the rung of the matching ladder that grouped them. MATERIAL_NUMBER and MANUFACTURER_PART_NUMBER are the rungs grouping uses today. NOT_GROUPED is one line that named neither a material number nor a manufacturer part number: it is its own product, and nothing joins it. Null on a product made any other way. enum: MATERIAL_NUMBER, MANUFACTURER_PART_NUMBER, SUPPLIER_PART_NUMBER, DESCRIPTION, NOT_GROUPED |
| sourceImportId | string | The transaction import whose grouping made this product. A bare uuid: the pipeline database is separate. Null on a product made any other way. A product the grouping made is deleted when regrouping or deleting that import leaves it holding no line. |
| meta | object |
Pipeline
Submit asynchronous analysis jobs such as cost-analysis pipelines.
/pipeline/cost-analysis
Submit a cost analysis pipeline request
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| x-app-user-id | header | string | optional | Gateway-injected app-user id of the acting user; persisted as the job's submittedById (DLN-296). Callers do not set this — the gateway strips inbound copies and supplies it from the JWT. |
| x-firebase-uid | header | string | optional | Gateway-set Firebase UID of the calling user (Langfuse user attribution). Server-injected — clients never supply it; the gateway strips inbound copies. |
| x-attributed-app-user-id | header | string | optional | Internal service-to-service attribution: the app user a service call acts on behalf of (e.g. the admin who approved an iteration). Honored ONLY for service-classified callers, and stripped from inbound requests by the gateway. |
| x-feature-flags | header | string | optional | Gateway-resolved feature flags for this (organization, user). Server-injected — clients never supply it; the gateway strips inbound copies. With TRANSACTIONS, a run for an existing productId whose roll-up has purchases is priced from it in the product's negotiation unit: quantity 1, pricesPaid the average price per negotiation unit, and the analysis reports in the purchases' currency. |
Request Body
| Field | Type | Description |
|---|---|---|
| id | string | Unique identifier for the pipeline request. |
| caVersion | string | The version of cost analysis generation process to use. |
| bomVersion | string | The version of bill of materials to use. |
| productName | string | Name of the product. Required for a normal run; for an iteration (previousAnalysisId set) it is derived from the prior run, so it may be omitted (DLN-197/ADR 0073). A new_analysis run (feedbackId without previousAnalysisId, DLN-196) is a normal run, so it must supply this. |
| manufacturerName | string | Name of the manufacturer. Required for a normal run; for an iteration (previousAnalysisId set) it is derived from the prior run, so it may be omitted (DLN-197/ADR 0073). A new_analysis run (feedbackId without previousAnalysisId, DLN-196) must supply this. |
| productId | string | ID of an existing product entity. When provided along with manufacturerId, skips AI company/product resolution and uses the existing entities. |
| manufacturerId | string | ID of an existing manufacturer entity. When provided along with productId, skips AI company/product resolution and uses the existing entities. |
| supplierNames | string[] | Supplier name(s). Each entry is resolved via a sequential AI company-resolution query, so the list is capped at 10. |
| supplierIds | string[] | IDs of existing supplier entities. When provided, skips AI resolution for supplierNames and links these existing suppliers directly. |
| productWebsite | string | Website of the product. |
| manufacturerWebsite | string | Website of the manufacturer. |
| productContext | string | Context about the product. |
| packagingDetails | string | The product's packaging, as the user typed it when promoting a SIGNAL product (ADR 0159). The BOM prompts read it after productContext. |
| specifications | string | The product's specifications, as the user typed them when promoting a SIGNAL product (ADR 0159). The BOM prompts read them after productContext. |
| processDescription | string | How the product is made, as the user described it when promoting a SIGNAL product (ADR 0159). The BOM prompts read it after productContext. |
| naicsCode | string | The product's industry as a six-digit NAICS code, chosen by the user. Outranks both the AI-resolved code and deterministic NAICS classification: the product stores it with naicsSource 'user', which classification leaves alone. A code from the prior NAICS revision is translated to its successor; a code that names no industry in either revision is refused. pattern: ^\d{6}$ |
| signalItemId | string<uuid> | The Signal item this analysis was started from ("Impact: Analyze this", DLN-1125). An item not found in the caller's tenant is refused with 404. When the item's HTS code is confirmed, the product stores that code with htsusSource 'signal' and the run derives none of its own; an unconfirmed or missing code changes nothing. Ignored on an iteration. uuid |
| manufacturerContext | string | Context about the manufacturer. |
| supplierContext | string | Context about the supplier(s). |
| purchaserContext | string | Context about the purchaser. |
| countryOfOrigin | string | ISO 3166-1 alpha-2 country code for the product's country of origin. pattern: ^[A-Z]{2}$ |
| importCountry | string | ISO 3166-1 alpha-2 country code where the product is imported to. pattern: ^[A-Z]{2}$default: "US" |
| reportingCurrency | string | ISO 4217 code of the currency the analysis is reported in; defaults to USD when absent. The engine computes in USD and converts the finished result at the stored annual rate; the USD figures are kept alongside. Prices paid are entered in this currency. A code with no rate for the run year fails the run. pattern: ^[A-Z]{3}$ |
| annualVolume | number | Expected annual purchase volume of the finished product. |
| annualVolumeUnit | string | Unit for annualVolume (e.g., kg, liter, unit). |
| unitType | string | Base unit of measurement for the product (e.g., kilogram, piece, liter). Overrides AI inference when provided. enum: piece, kilogram, gram, liter, milliliter, meter, foot, square meter, square foot, cubic meter, gallon, pound, ounce, ton, sheet, roll, barrel, board foot |
| countryOfOriginText | string | Raw, unnormalized country-of-origin text from a bulk import (e.g. 'China', 'P.R.C.'). Unlike countryOfOrigin, it is NOT validated to alpha-2; Product Resolution normalizes it. Used only by the import path — the form/API should send the strict countryOfOrigin. |
| unitTypeText | string | Raw, unnormalized unit-of-measure text from a bulk import (e.g. 'kg', 'tonne'). Unlike unitType, it is NOT validated to the closed enum; Product Resolution normalizes it. Used only by the import path — the form/API should send the strict unitType. |
| quantity | number | Quantity of the product for this cost analysis (e.g., 2 for 2 tons, 1000 for 1000 units). Decimals supported. Defaults to 1 if not specified. |
| pricesPaid | PricesPaidItemDto[] | Historical prices paid, entered in the reporting currency. On a run that reports in a currency other than USD every item must be in that currency; a mismatch is a 400 naming both. |
| date | string | Date when the price was paid. |
| price* | number | Price paid in minor currency units (e.g., cents). |
| currency* | string | ISO 4217 currency code (e.g., USD, EUR). Case and surrounding whitespace are tidied before validation, so a bulk-import cell of 'usd' or 'CAD ' is read as USD or CAD. pattern: ^[A-Z]{3}$ |
| enableBomJudge | boolean | Retired (DLN-1074): accepted for backward compatibility and ignored. The BOM Judge step no longer runs. |
| enableTier1Adjustments | boolean | If true, apply Tier 1 process parameter adjustments (compliance overhead and batch setup amortization) derived from the BOM process parameters. When false, the ASM industry averages are used without process-level adjustments. default: false |
| enableSupplierPower | boolean | If true, apply the supplier purchasing-power step: components are priced at the supplier-facing cost (reference price x a capped, scale-based discount factor). Set false to keep every component at the universal reference price. default: true |
| supplierScaleTier | string | Optional explicit supplier scale tier, used as the purchasing-power signal when no financials are available (fallback of last resort before no-discount). enum: enterprise, mid, small |
| enableProcessAssessment | boolean | If true, run a process assessment step that generates Tier 2 adjustments. default: false |
| costAdjustments | CostAdjustmentItemDto[] | Explicit named adjustments to apply to cost-analysis components. |
| name* | string | Human-readable label for this adjustment. |
| category* | string | Cost category type to target. enum: direct_materials, indirect_materials, labor, overhead |
| type | string | How to apply the adjustment. enum: multiplicative, additivedefault: "multiplicative" |
| factor | number | Multiplicative factor (e.g., 1.1 for a 10% increase). |
| amount | number | Additive amount in minor currency units (cents). |
| enableQa | boolean | If true, run an AI-powered QA evaluation step after cost analysis. default: false |
| enableAiTariffResolution | boolean | If true, run an AI-powered Tariff Resolution step (Step 3d) to determine the effective import tariff rate, replacing the static HTSUS dataset lookup. Supports non-US import countries when enabled. default: true |
| annualRevenue | number | DEPRECATED (DLN-73): Manufacturer annual revenue. Prefer POST /company/{id}/financials with source=manual. Hard-overrides the EDGAR auto-fetch for this run. |
| annualCogs | number | DEPRECATED (DLN-73): Manufacturer annual cost of goods sold. Prefer POST /company/{id}/financials with source=manual. |
| annualSga | number | DEPRECATED (DLN-73): Manufacturer annual SG&A expenses. Prefer POST /company/{id}/financials with source=manual. |
| enableMonteCarlo | boolean | If true, run Monte Carlo simulation to compute costMin/costMax. Bounds derive from the MC P10/P90 but are widened where needed so the deterministic cost always lies within [costMin, costMax]; a widened side is a containment bound, not a strict percentile. default: true |
| deterministicClassification | any | Deterministic code classification toggles, one per taxonomy. NAICS and UNSPSC default per environment (on in development and staging, off in production, via DETERMINISTIC_CLASSIFICATION_DEFAULT_ON on the cost-analysis job) and each key defaults independently, so {"naics": false} leaves UNSPSC at its default. Defaults are documentation only — the job side applies them, in jobs/cost-analysis/lib/options.py. |
| analysisConfig | string | Named analysis configuration preset (e.g., "basic"). |
| existingBomId | string | ID of an existing bill of materials to reuse. When provided, the pipeline skips BOM generation and uses this BOM for downstream steps. If bomContext is also provided, the BOM is revised rather than reused as-is. |
| bomContext | string | Free-text instructions for revising an existing BOM. Only used when existingBomId is also provided. Describes what should change from the previous BOM (e.g., "change support legs from 2 to 4 pieces"). |
| previousAnalysisId | string | DLN-197/ADR 0073: the cost analysis being iterated on (with-rerun). This field — together with feedbackId — is the iteration discriminator: when both are set, the orchestrator resolves the prior run via its computeInputs.sourceJobId, reuses its entities + BOM, and revises the BOM from the feedback corrections (so productName/manufacturerName are derived and may be omitted). Omit it for a normal or new_analysis run. |
| feedbackId | string | DLN-197/ADR 0073: the analysis-feedback record this run fulfils. With previousAnalysisId it drives an iteration rerun (the orchestrator reads its normalizedCorrections as the single source of truth for the BOM revision). Without previousAnalysisId it marks a new_analysis run (DLN-196) for audit: the orchestrator stamps this feedback record’s resultAnalysisId with the new CostAnalysis it creates. |
| includeBomPackaging | boolean | Include packaging components in the BOM. When false, the AI is instructed to omit packaging from the BOM and packaging costs use ASM industry averages. default: true |
| includeBomWaste | boolean | Include waste components in the BOM. When false, the AI is instructed to omit waste from the BOM and waste removal costs use ASM industry averages. default: true |
| includeBomConsumables | boolean | Include consumable components in the BOM. When false, the AI is instructed to omit consumables from the BOM and consumable costs use ASM industry averages. default: true |
| includeBomByproducts | boolean | Include valuable byproduct/co-product components in the BOM. When false, the AI is instructed to omit byproducts and no byproduct credit is taken off the total. default: true |
| importId | string<uuid> | Parent Import when this run is one row of a file import uuid |
| rowIndex | number | Zero-based row index in the source import file min: 0 |
| rowData | object | Raw parsed source row (import provenance) |
Responses
| Field | Type | Description |
|---|---|---|
| data | JobDto | |
| id | string | Unique identifier for the job |
| parentJobId | string | FK to a parent job when this job is a sub-task of a pipeline job |
| importId | string | FK to the parent Import when this job is one row of a file import |
| rowIndex | number | Zero-based row index within the source import file (set for import rows) |
| type* | string | Job type identifier (e.g., bom_generation, cost_analysis) |
| status* | string | Current execution status of the job enum: pending, in_progress, completed, failed, skipped |
| dispatchClaimedAt | string | Dispatch lease (DLN-329). Cloud Tasks delivers at-least-once, so the dispatch worker claims a run by compare-and-set on this column before calling the Cloud Run Jobs API; a delivery that loses the race skips. Set while `status` is still 'pending' — the orchestrator, not the worker, owns the move to 'in_progress'. The lease expires (see DISPATCH_CLAIM_LEASE_MS) so a worker that dies mid-claim doesn't strand the run: the next redelivery re-claims it. |
| dispatchRequestedAt | string | When a launch was last requested for this run (DLN-613): stamped at creation and re-stamped by every retry, alongside the reset of `started`/`dispatchClaimedAt`. Exists so a run that is dispatched but never claimed — enqueueDispatch failing outright, or every Cloud Tasks redelivery of it being exhausted — still has something to age against. `started` and `dispatchClaimedAt` take precedence when present; this is the last-resort fallback the isRunStalled predicate uses when both of those are null. Deliberately reset on retry (unlike createdAt) so a retried run ages from the retry, not its original submission — see stale-run.constants.ts for why that distinction matters. |
| autoRetryCount | number | How many times this run has been retried automatically since it was last retried by a person. Automatic retry stops once this reaches its limit; retrying the run by hand resets it to 0. Absent on runs created before automatic retry existed, which means 0. |
| payload | object | Input data for the job (structure depends on job type) |
| result | object | Output data from the job (structure depends on job type) |
| step | string | Current step label for progress tracking (e.g., fetching_data, analyzing) |
| started | string | Timestamp when the job started executing |
| completed | string | Timestamp when the job completed successfully |
| errored | string | Timestamp when the job encountered an error, if applicable |
| errorMessage | string | Error message describing what went wrong, if applicable |
| source | string | How this cost-analysis job was triggered. Derived server-side at creation from the request shape (DLN-296): manual = direct Add Product; bulk_import = one row of a file import (see importId); iteration = feedback-approved rerun of an existing analysis; iteration_new_product = feedback approval that forks a new product/analysis. enum: manual, bulk_import, iteration, iteration_new_product |
| submittedById | string | App-user id (x-app-user-id) of the user who triggered the job — the submitter for a manual run, the approving admin for an iteration/new-product run, or the person who asked for a bom_refresh. A plain id string (not necessarily a UUID); resolve to a display name via user-service. |
| meta | object |
/pipeline/cost-analysis/{id}
Get a cost analysis or bom-refresh job by ID
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required |
Responses
| Field | Type | Description |
|---|---|---|
| data | CostAnalysisJobDto | |
| id | string | Unique identifier for the job |
| parentJobId | string | FK to a parent job when this job is a sub-task of a pipeline job |
| importId | string | FK to the parent Import when this job is one row of a file import |
| rowIndex | number | Zero-based row index within the source import file (set for import rows) |
| type* | string | Job type identifier (e.g., bom_generation, cost_analysis) |
| status* | string | Current execution status of the job enum: pending, in_progress, completed, failed, skipped |
| dispatchClaimedAt | string | Dispatch lease (DLN-329). Cloud Tasks delivers at-least-once, so the dispatch worker claims a run by compare-and-set on this column before calling the Cloud Run Jobs API; a delivery that loses the race skips. Set while `status` is still 'pending' — the orchestrator, not the worker, owns the move to 'in_progress'. The lease expires (see DISPATCH_CLAIM_LEASE_MS) so a worker that dies mid-claim doesn't strand the run: the next redelivery re-claims it. |
| dispatchRequestedAt | string | When a launch was last requested for this run (DLN-613): stamped at creation and re-stamped by every retry, alongside the reset of `started`/`dispatchClaimedAt`. Exists so a run that is dispatched but never claimed — enqueueDispatch failing outright, or every Cloud Tasks redelivery of it being exhausted — still has something to age against. `started` and `dispatchClaimedAt` take precedence when present; this is the last-resort fallback the isRunStalled predicate uses when both of those are null. Deliberately reset on retry (unlike createdAt) so a retried run ages from the retry, not its original submission — see stale-run.constants.ts for why that distinction matters. |
| autoRetryCount | number | How many times this run has been retried automatically since it was last retried by a person. Automatic retry stops once this reaches its limit; retrying the run by hand resets it to 0. Absent on runs created before automatic retry existed, which means 0. |
| payload | object | Input data for the job (structure depends on job type) |
| step | string | Current step label for progress tracking (e.g., fetching_data, analyzing) |
| started | string | Timestamp when the job started executing |
| completed | string | Timestamp when the job completed successfully |
| errored | string | Timestamp when the job encountered an error, if applicable |
| errorMessage | string | Error message describing what went wrong, if applicable |
| source | string | How this cost-analysis job was triggered. Derived server-side at creation from the request shape (DLN-296): manual = direct Add Product; bulk_import = one row of a file import (see importId); iteration = feedback-approved rerun of an existing analysis; iteration_new_product = feedback approval that forks a new product/analysis. enum: manual, bulk_import, iteration, iteration_new_product |
| submittedById | string | App-user id (x-app-user-id) of the user who triggered the job — the submitter for a manual run, the approving admin for an iteration/new-product run, or the person who asked for a bom_refresh. A plain id string (not necessarily a UUID); resolve to a display name via user-service. |
| result | any | Result shape depends on the job type: cost_analysis jobs return this cost-analysis result; a bom_refresh job's result instead carries the refresh outcome — per-outcome refresh counts (update / ignore / flag-for-review / preserved / held / total, plus anchored: the parts priced from the customs anchor, already counted under update), materiality (material, nodeCrossings, rollupCrossed), catalog/lookup counts (catalogEntries / lookups / lookupsSaved), and analysisId when a material price move minted a new version (absent when prices were checked with no material change). |
| stalled* | boolean | Derived, not stored: true when the run still reads pending or in_progress long after a live execution would have reported. A run whose container dies keeps that status forever — nothing writes a correction, and the Cloud Run task retry that used to mask it was removed — so `status` alone reports a dead run as running. Aged against the job timeout plus a clock-skew margin, from `started`; before the orchestrator sets that, from `dispatchClaimedAt`; and before a dispatch is claimed at all, from `dispatchRequestedAt`. That last case also catches a run whose dispatch is merely still queued behind the dispatch rate limit, so on a large import true can mean "not started yet" rather than "died". Nothing clears the flag on its own: a stalled run stays stalled until a retry re-stamps its anchor. POST /pipeline/cost-analysis/:id/retry is that path, and it can still refuse — 409 when the run already produced an analysis or the row changed underneath the retry, 400 for a bom_refresh whose payload carries no productId. |
| meta | object |
Cost Analysis
Retrieve completed cost-analysis results and component-level details.
/cost-analysis/{id}/feedback
List feedback submitted on a cost analysis (DLN-311)
Returns the feedback submissions for the analysis, newest-first, tenant-scoped. An analysis with no feedback yet returns an empty list. Responds 404 only when the cost analysis is unknown or belongs to another tenant.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required | |
| limit | query | number | optional | |
| offset | query | number | optional | |
| count | query | boolean | optional | Include totalCount of unbounded results in response meta |
Responses
| Field | Type | Description |
|---|---|---|
| data | AnalysisFeedbackDto[] | |
| id* | string | Unique identifier for the analysis-feedback record |
| costAnalysis* | any | The cost analysis this feedback is for (the ticket's analysisId). Validated relation — accepts the related object or its id string (@ValidateRelatedOrId). - accepts full object, id string, or id number |
| submittedById* | string | User who submitted the feedback |
| submittedByName | string | Display name of the submitter, captured at submission time |
| submittedAt* | string | When the feedback was submitted |
| origin | string | Whether the iteration was submitted by a Dalinea-internal or an external (customer) user. Derived server-side at submit from the gateway-set x-user-type header (never the request body). Internal-origin iterations are excluded from the rerun count/cap and hidden from customer-facing history (DLN-334). Absent on rows predating DLN-334 → treated as external. enum: internal, external |
| changeType* | string | Kind of change the feedback requests enum: new_analysis, iteration_no_rerun, iteration_with_rerun |
| feedbackStatus* | string | Review lifecycle status enum: auto_approved, submitted, under_review, approved, rejected |
| corrections | any | The user-submitted correction payload. Bundled rather than flattened: the individual values are reported on, not filtered. The concrete grammar of the free-form sub-objects is owned by the submit flow (DLN-194). |
| normalizedCorrections | any | The admin-normalized counterpart of `corrections`, produced during review (DLN-195). Same field set, plus `changeType` when the admin reclassifies the request. |
| reviewedById | string | Admin who reviewed the feedback |
| reviewedByName | string | reviewedByName |
| reviewedAt | string | reviewedAt |
| releasedReason | string | Set when the system, not a reviewer, rejected a with-rerun iteration because its run will never produce a version: run_failed when the run failed or died, dispatch_failed when the pipeline refused to start it. A rejection carrying it ends the iteration round, so the version it ran from is no longer "in progress"; a reviewer's rejection never does. enum: run_failed, dispatch_failed |
| bomValidationOverrides | string[] | BOM validation rules the admin chose to override for this iteration. Each item identifies a rule and the override decision; shape owned by the admin review flow (DLN-195). |
| resultAnalysisId | string | Nullable pointer to the resulting cost analysis (the new v1 for new_analysis, or the new version for iterations). Stored as a plain uuid (deferred-FK) — a second validated relation to cost-analysis is deferred to avoid multi-relation disambiguation; AC requires only the submitted analysis FK to be validated. |
| evaluationMessage | string | System/admin message describing the evaluation outcome |
| meta | object | |
| totalCount | integer | Unbounded result count. Only present when ?count=true is passed. |
/cost-analysis/{id}/feedback
Submit category-manager feedback on a cost analysis (DLN-194)
For external submitters, the first submission claims the feedback-owner lock (403 for a different user). DLN-334: internal (Dalinea) submitters are exempt — they neither claim the lock nor are blocked by an existing owner (no 403), and their iterations do not count toward or hit the 3-round rerun cap. iteration_with_rerun and new_analysis enter the admin review queue (202, status → pending_review); iteration_with_rerun is rejected with 422 once the chain has consumed 3 rerun rounds (external only). iteration_no_rerun recomputes synchronously (no AI) and resolves immediately (200) with the auto-approved feedback pointing at the newly persisted version via resultAnalysisId. In an organization holding BOTH the EXTERNAL_COST_ANALYSIS_RUNS and EXTERNAL_ITERATION_AUTO_EXECUTE overrides, an EXTERNAL iteration_with_rerun skips the queue entirely — it is stored auto_approved and the rerun is dispatched on submit (202). Holding EXTERNAL_COST_ANALYSIS_RUNS alone permits an external admin to approve a run by hand but does NOT skip the queue. The round cap and the first-submitter lock still apply, and new_analysis, internal submitters and unflagged organizations are unaffected.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required | |
| x-app-user-id | header | string | required | |
| x-user-type | header | string | optional | Gateway-set INTERNAL/EXTERNAL user type (DLN-334). Server-injected — clients never supply it; the gateway strips inbound copies. Absent → treated as external. |
| x-feature-flags | header | string | optional | Gateway-resolved feature flags for this (organization, user). Server-injected — clients never supply it; the gateway strips inbound copies. Absent → no flags, i.e. the submission is queued for review. |
| x-user-roles | header | string | optional | Gateway-resolved role names for this (organization, user). Server-injected — clients never supply it; the gateway strips inbound copies. Read only by the manufacturer-financials override, which needs DALINEA_ADMIN. Absent → no roles. |
Request Body
| Field | Type | Description |
|---|---|---|
| changeType* | string | Kind of change requested. iteration_no_rerun resolves immediately; iteration_with_rerun and new_analysis enter the admin review queue. enum: new_analysis, iteration_no_rerun, iteration_with_rerun |
| submittedByName | string | Submitter display name, captured for the review queue. Non-authoritative — the identity used for the ownership lock comes from x-app-user-id. |
| corrections | any | The user-submitted correction payload (per the analysis-feedback grammar). |
Responses
| Field | Type | Description |
|---|---|---|
| data | AnalysisFeedbackDto | |
| id* | string | Unique identifier for the analysis-feedback record |
| costAnalysis* | any | The cost analysis this feedback is for (the ticket's analysisId). Validated relation — accepts the related object or its id string (@ValidateRelatedOrId). - accepts full object, id string, or id number |
| submittedById* | string | User who submitted the feedback |
| submittedByName | string | Display name of the submitter, captured at submission time |
| submittedAt* | string | When the feedback was submitted |
| origin | string | Whether the iteration was submitted by a Dalinea-internal or an external (customer) user. Derived server-side at submit from the gateway-set x-user-type header (never the request body). Internal-origin iterations are excluded from the rerun count/cap and hidden from customer-facing history (DLN-334). Absent on rows predating DLN-334 → treated as external. enum: internal, external |
| changeType* | string | Kind of change the feedback requests enum: new_analysis, iteration_no_rerun, iteration_with_rerun |
| feedbackStatus* | string | Review lifecycle status enum: auto_approved, submitted, under_review, approved, rejected |
| corrections | any | The user-submitted correction payload. Bundled rather than flattened: the individual values are reported on, not filtered. The concrete grammar of the free-form sub-objects is owned by the submit flow (DLN-194). |
| normalizedCorrections | any | The admin-normalized counterpart of `corrections`, produced during review (DLN-195). Same field set, plus `changeType` when the admin reclassifies the request. |
| reviewedById | string | Admin who reviewed the feedback |
| reviewedByName | string | reviewedByName |
| reviewedAt | string | reviewedAt |
| releasedReason | string | Set when the system, not a reviewer, rejected a with-rerun iteration because its run will never produce a version: run_failed when the run failed or died, dispatch_failed when the pipeline refused to start it. A rejection carrying it ends the iteration round, so the version it ran from is no longer "in progress"; a reviewer's rejection never does. enum: run_failed, dispatch_failed |
| bomValidationOverrides | string[] | BOM validation rules the admin chose to override for this iteration. Each item identifies a rule and the override decision; shape owned by the admin review flow (DLN-195). |
| resultAnalysisId | string | Nullable pointer to the resulting cost analysis (the new v1 for new_analysis, or the new version for iterations). Stored as a plain uuid (deferred-FK) — a second validated relation to cost-analysis is deferred to avoid multi-relation disambiguation; AC requires only the submitted analysis FK to be validated. |
| evaluationMessage | string | System/admin message describing the evaluation outcome |
| meta | object |
/cost-analysis/{id}/review
Mark a cost-analysis version reviewed
Records that a person at the customer has looked over this version and found it right. Appends a review to the version's history and sets its latest-review fields (reviewedById, reviewedByName, reviewedAt, reviewedCostAnalysisId, reviewedVersionLabel) in one transaction. Nothing else changes: no new version, no run, no rerun round, and the feedback owner is neither checked nor claimed. Reviewing the same version again adds another review and the latest one shows. Open to every customer role (Viewer and up); Dalinea staff are refused.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required | |
| x-app-user-id | header | string | optional | Gateway-set identity of the reviewer. Server-injected; a request without it is refused with 400. |
| x-user-type | header | string | optional | Gateway-set INTERNAL/EXTERNAL user type. Server-injected; any caller that is not EXTERNAL is refused with 403. |
Request Body
| Field | Type | Description |
|---|---|---|
| reviewerName | string | Display name of the reviewer, shown as "Reviewed by NAME". Attribution only — the reviewer is identified by x-app-user-id. maxLen: 255 |
| versionLabel | string | The version label the reviewer saw, such as "v3". Display-only, like reviewerName: stored as the review's versionLabel and the version's reviewedVersionLabel. Absent, the version's stored versionLabel is used. The version itself is identified by the path id. maxLen: 32 |
Responses
| Field | Type | Description |
|---|---|---|
| data | CostAnalysisReviewDto | |
| id* | string | Unique identifier for the review row |
| costAnalysisId* | string | The @cost-analysis version that was reviewed. |
| productId* | string | Copied from the analysis so the history reads per product on its own. |
| versionLabel | string | versionLabel of the reviewed analysis, copied at review time. |
| reviewerUserId* | string | The reviewing user, from the request's x-app-user-id header. |
| reviewerName | string | Display name of the reviewer, as supplied on the review request. Attribution only; reviewerUserId is the authoritative link. Nullable. |
| reviewedAt* | string | When the review was recorded. Server-stamped. |
| meta | object |
/cost-analysis/{id}/reviews
List the reviews of a cost-analysis version
Every review recorded directly on this version, newest first. The version's latest-review fields can instead name a review of an earlier version that a refresh carried forward.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required |
Responses
| Field | Type | Description |
|---|---|---|
| data | CostAnalysisReviewDto[] | |
| id* | string | Unique identifier for the review row |
| costAnalysisId* | string | The @cost-analysis version that was reviewed. |
| productId* | string | Copied from the analysis so the history reads per product on its own. |
| versionLabel | string | versionLabel of the reviewed analysis, copied at review time. |
| reviewerUserId* | string | The reviewing user, from the request's x-app-user-id header. |
| reviewerName | string | Display name of the reviewer, as supplied on the review request. Attribution only; reviewerUserId is the authoritative link. Nullable. |
| reviewedAt* | string | When the review was recorded. Server-stamped. |
| meta | object | |
| totalCount | integer | Unbounded result count. Only present when ?count=true is passed. |
/cost-analysis/portfolio/summary
Portfolio Overview savings-potential summary
Aggregates each product's primary cost analysis into Likely/Possible/Unlikely/Unknown savings buckets (counts, percentages, summed savings potential in minor currency units) plus the top-N savings opportunities. A product with no resolvable primary contributes its latest completed analysis instead. Soft-deleted analyses never contribute. Every total is in USD, the currency each analysis is computed in; mixedCurrencies flags a book holding any product that reports in another currency, and each row carries its reportingCurrency and presented figures. Tenant-wide, cached briefly in memory.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| topLimit | query | integer | optional | Rows in topProducts (default 5, max 50) |
| recentLimit | query | integer | optional | Rows in recentAnalyses (default 5, max 50) |
Responses
| Field | Type | Description |
|---|---|---|
| data | PortfolioSummaryDto | |
| currency* | string | Denomination of every total in this summary: always USD. The buckets sum the USD base figures of each contributing analysis whatever currency that analysis reports in. |
| mixedCurrencies* | boolean | True when any contributing analysis reports in a currency other than USD. The totals are still USD; the per-row reportingCurrency and presented figures say what each product shows. |
| totalProducts* | number | Products with >=1 completed cost analysis — the percentage denominator |
| buckets* | PortfolioBucketsDto | |
| likely* | PortfolioBucketStatDto | |
| possible* | PortfolioBucketStatDto | |
| unlikely* | PortfolioBucketStatDto | |
| unknown* | any | Products whose contributing analysis has no savings opportunity or no usable should-cost |
| topProducts* | PortfolioTopProductDto[] | |
| productId* | string<uuid> | Product the opportunity is for uuid |
| costAnalysisId* | string<uuid> | Contributing cost analysis id — the product's primary, or its latest completed analysis when no primary resolves uuid |
| savingsOpportunityId* | string<uuid> | Savings opportunity id uuid |
| shouldCostAmount* | number | Should-cost in minor currency units |
| savingsAmount* | number | Savings amount (reference price - should-cost) in minor units |
| savingsPct* | number | Savings as a fraction of should-cost (0..1) |
| bucket* | string | Bucket this product falls into enum: likely, possible, unlikely, unknown |
| currency* | string | Denomination of the base figures on this row: always USD, the currency every analysis is computed in. |
| reportingCurrency* | string | The currency the contributing analysis reports in (USD unless the run asked for another). When it differs from currency, `presented` carries the figures in it. |
| presented* | any | The base figures in reportingCurrency, read from the stored twins. Null when the analysis reports in USD. |
| recentAnalyses* | PortfolioRecentAnalysisDto[] | |
| productId* | string<uuid> | Product the analysis is for uuid |
| costAnalysisId* | string<uuid> | Cost analysis id uuid |
| analysisDate* | string | When the analysis was performed (ISO 8601) |
| shouldCostAmount* | number | Should-cost in minor currency units |
| savingsAmount* | number | Savings amount in minor units; null when the analysis has no opportunity |
| savingsPct* | number | Savings as a fraction of should-cost (0..1); null when no opportunity or no usable should-cost |
| currency* | string | Denomination of the base figures on this row: always USD, the currency every analysis is computed in. |
| reportingCurrency* | string | The currency the contributing analysis reports in (USD unless the run asked for another). When it differs from currency, `presented` carries the figures in it. |
| presented* | any | The base figures in reportingCurrency, read from the stored twins. Null when the analysis reports in USD. |
| generatedAt* | string | When the summary was computed (ISO 8601) |
| meta | object |
/cost-analysis/by-product/{productId}
Get a product's primary cost analysis
Returns the product's PRIMARY cost analysis — the analysis of record. A product with no primary, or whose primary does not resolve to a live completed analysis of its own within the requested filterScope, falls back to its latest completed analysis, ordered exactly as the portfolio aggregates order theirs (newest analysisDate, then highest id — analysisDate alone is caller-supplied and can tie). Called without filterScope, this normally names the same analysis the portfolio aggregates counted for that product. Two things make the two surfaces differ. First, the aggregates are cached per tenant for up to 90 seconds — up to an hour while the primary-analysis pointer is unresolvable and a previously computed answer is being served — and nothing invalidates that cache when a primary moves, so for that window a dashboard figure can still name the previous analysis while this lookup already names the new one. Second, the aggregates apply no filterScope: request one here and a product whose primary sits outside it is answered from its latest in-scope analysis instead, or 404s if it has none, while the aggregates go on counting the pinned one. The answer is a single merged object (run-level record + rollup buckets as components). Monetary values are in minor currency units. Responds 202 with job status when no completed analysis exists but a pipeline run is pending or in progress (the in-progress check is product-level and not constrained by filterScope). Responds 404 when neither exists, including when the pipeline service is unreachable. Responds 503 when a completed analysis exists but the primary could not be resolved: the endpoint reports that rather than answering from a different basis.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| productId | path | string<uuid> | required | |
| filterScope | query | string | optional | Comma-separated dimension:value filters constraining which completed analyses qualify, as the primary and as the fallback alike. The portfolio aggregates apply no such filter, so requesting one is the one case where this endpoint deliberately names a different analysis than the dashboard figure for the same product. Supported dimensions: countryOfOrigin, importCountry. Example: countryOfOrigin:CN,importCountry:US |
Responses
| Field | Type | Description |
|---|---|---|
| data | CostAnalysisResultDto1 | |
| id* | string<uuid> | Cost analysis id uuid |
| status* | string | Status of the cost analysis enum: analysis_done |
| analysisDate* | string | When the analysis was performed (ISO 8601) |
| productId* | string<uuid> | Product the analysis belongs to uuid |
| currency | string | Currency code for all monetary values (e.g., USD) |
| bomId | string<uuid> | Bill of materials the analysis was computed from uuid |
| countryOfOrigin | string | ISO country code of origin |
| importCountry | string | ISO country code of import destination |
| shouldCostAmount | number | Theoretical production cost in minor currency units |
| shouldCostStdErr | number | Standard error of the should-cost estimate in minor currency units |
| qaVerdict | object | QA Evaluation verdict (DLN-158). Null when QA was not enabled for this analysis. |
| adjustments | AdjustmentsDto[] | Adjustments applied beyond the industry-average baseline |
| type* | string | Identifier for the kind of adjustment applied enum: manufacturer_financials, yield_adjustment, compliance_overhead, setup_amortization, named_adjustment, tariff, indirect_tariff, volume_discount, supplier_purchasing_power |
| source* | string | Origin of this adjustment enum: user_provided, ai_estimate, analyst_judgment, system, edgar_auto, operator_entry |
| label | string | Human-readable description of what was adjusted |
| metadata | object | Adjustment-specific qualitative context (no dollar amounts). For type="manufacturer_financials": may include - cogsRatioAdjusted (boolean) - profitMarginOverridden (boolean) - fiscalYear (integer): the FY the financials describe - companyFinancialsId (uuid): the specific company_financials row that fed this run, enabling audit even after the cache rolls to a newer filing - financialsSource ("edgar" | "manual" | "llm" | "dto"): where the financials came from for this run - sgaCompleteness ({found:[...], missing:[...]}): presence of each financial figure the SG&A selection reads — revenue, cogs, the sm/ga pair, R&D, and the combined SG&A line — plus the reported operating income, tracked here as drift-detection evidence even though DLN-913 margin derivation never reads it: the operating margin comes only from revenue, cogs and SG&A. `missing` is not a defect signal: a filer that legitimately never files a given tag (e.g. a decomposed filer with no combined line) reports it missing without anything being wrong. - operatingMarginSource ("edgar_sga" | "asm_industry_average"): which route produced the margin PAIR. "edgar_sga" — the manufacturer's own margins, the operating half reconstructed from an SG&A sum: `(revenue - cogs - sga) / revenue` (the name is a misnomer for the manual-DTO case too; kept for backward compatibility). This is the ONLY way a manufacturer's own operating margin can be adopted. DLN-913 reversed an earlier decision (PR #636) to prefer a filer's own reported `us-gaap:OperatingIncomeLoss`: we cannot know what a filer included when it calculated that figure, so we derive the margin ourselves and never adopt theirs, however it compares to our own gross margin. "edgar_operating_income" was that now-retired source value; it can no longer appear. "asm_industry_average" — BOTH margins came from the industry baseline. Since DLN-913 the manufacturer's gross and operating margin are adopted together or not at all, so this value means the whole pair is industry-sourced; before DLN-913 a run could carry the manufacturer's gross margin beside an industry or derived operating margin. - industryAverageReason ("no_margin_pair" | "implausible_gross_margin"): why the run fell back. Present only when operatingMarginSource is "asm_industry_average". "no_margin_pair" — financials reached the engine (at least one of COGS, SG&A or the GROSS margin was supplied — an operating margin alone does not emit this adjustment) but did not yield both margins, so neither was adopted; this covers a half pair whose present half is the gross margin, and a filing that resolved some figure but neither margin (e.g. SG&A with no revenue). "implausible_gross_margin" — the DLN-888 band withheld the whole set. A run with NO manufacturer financials at all (nothing supplied) emits no manufacturer_financials adjustment and so carries neither this key nor operatingMarginSource — the frontend's own `financialsStatus`/`financialsSource` fields on computeInputs are what explain that case, not this metadata; so does a run whose only supplied figure is an operating margin — absence of the adjustment does not by itself mean nothing was supplied. - llmCitations ([{field, url, note}]): present only when financialsSource == "llm" For type="tariff" (direct product-level import tariff): may include - importCountry (string) - countryOfOrigin (string) - ratePercent (number) For type="indirect_tariff" (tariffs on foreign-sourced BOM components): may include - components (object): per-component breakdown of the indirect tariff impact |
| components* | CostComponentDto[] | Cost breakdown by rollup bucket |
| id* | string<uuid> | Unique identifier of the bucket row uuid |
| name* | string | Name of the cost component (e.g., "Direct Materials") |
| currency* | string | Currency code (e.g., USD) |
| cost* | number | Cost amount in minor currency units (e.g., cents) |
| costMin* | number | Minimum estimated cost in minor currency units |
| costMax* | number | Maximum estimated cost in minor currency units |
| meta | object |
| Field | Type | Description |
|---|---|---|
| data | CostAnalysisInProgressDto | |
| jobId* | string<uuid> | Pipeline job id to poll via GET /pipeline/cost-analysis/:id uuid |
| status* | string | Job status enum: pending, in_progress |
| step | string | Current pipeline step label |
| meta | object |
/cost-analysis/{id}/versions
Get the full version history of a cost analysis
Returns every version in the analysis chain in chronological order (oldest first). Accepts ANY version id in the chain — it walks both backward (prior versions) and forward (later iterations) from the given id. Each entry is a summary; full data stays on GET /cost-analysis/:id. Responds 404 when the id is unknown or belongs to another tenant.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required |
Responses
| Field | Type | Description |
|---|---|---|
| data | CostAnalysisVersionDto[] | |
| id* | string<uuid> | Cost analysis id for this version uuid |
| versionLabel* | string | Human-readable label for this version |
| changeType* | string | Iterations changeType of the feedback that produced this version; null for the root (no driving feedback) |
| status* | string | Lifecycle status of this version |
| feedbackOwnerId* | string<uuid> | User who owns the feedback loop for this version uuid |
| feedbackOwnerName* | string | Display name of the feedback owner |
| changeSummary* | string | Short summary of what changed in this version |
| feedbackSummary* | string | Short summary of the feedback that drove this version |
| createdAt* | string | When this version was created (ISO 8601) |
| hasBomValidationOverrides* | boolean | True when the driving feedback carries confirmed BOM validation overrides |
| rerunInFlight* | boolean | True while an approved rerun from this version has not produced its result. A version whose rerun finished keeps the status pending_feedback, so read this, not the status, to know whether it is busy. |
| meta | object | |
| totalCount | integer | Unbounded result count. Only present when ?count=true is passed. |
/cost-analysis/{id}
Get cost-analysis by ID
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required | |
| related | query | string[] | optional | Include related entities. Omit for all, "false" for none, or specify field names (repeatable, supports dot notation) |
Responses
| Field | Type | Description |
|---|---|---|
| data | CostAnalysisDto | |
| id* | string | Unique identifier for the cost analysis record |
| status* | string | Status of the cost analysis lifecycle. analysis_done = an AI pipeline run finished (the former "completed"); pending_review / pending_feedback / iteration are the human-in-the-loop iterations states (DLN-190). enum: pending, analysis_done, pending_review, pending_feedback, iteration, failed |
| analysisDate* | string | Date and time when the cost analysis was performed |
| productId | string | FK to the @product for which the cost analysis is performed |
| currency | string | Denomination of the base money columns on this analysis and its children (shouldCostAmount, bucket and component costs, reference prices, savings). The engine computes in USD, so this is USD. The currency the customer sees is reportingCurrency; its figures live in the `presented` twins. |
| reportingCurrency | string | The currency the analysis is reported in. When it differs from `currency`, every money-bearing row carries a `presented` twin in this currency, converted from the USD base at fxRate. Rows produced before this field existed are USD. |
| fxRate | number | Units of reportingCurrency per one US dollar, as stored on the annual exchange-rate row cited by fxRateId at the time of the run. Fixed on the analysis so re-opening it later shows the same figures whatever the rate table has done since. Null when reportingCurrency is USD. |
| fxRateSource | string | Publisher of the rate, copied from the exchange-rate row (e.g. Bank of Canada). Null when USD. |
| fxRateAsOf | string | The date the rate reflects, copied from the exchange-rate row. Null when USD. |
| fxRateId | string | The exchange-rate row the conversion cites, for audit. Null when USD. |
| presented | any | The headline figures in reportingCurrency: each is round_half_up(base cents x fxRate). Null when reportingCurrency is USD. |
| bomId | string | FK to the bill of materials used as the basis for this cost analysis |
| countryOfOrigin | string | The country of origin for the product. This may impact the cost analysis due to factors like tariffs, shipping costs, and local market conditions. |
| importCountry | string | The country into which the product is imported. Used for tariff rate lookups. Defaults to US if not provided. |
| shouldCostAmount | number | Theoretical cost to produce the product, in minor currency units (e.g., cents) |
| shouldCostStdErr | number | Approximation of the standard error of the should-cost estimate, in minor currency units (e.g., cents) |
| scenarioContext | string | Named rate scenario this analysis ran under. Nullable. |
| analysisBasis | string | Basis of the analysis (e.g. 'should_cost'). |
| priorCostAnalysisId | string | Self-FK to the previous CA in a temporal lineage. Nullable (DLN-152 writes). |
| changeType | string | Classification of why this CA exists: BOM_REVISION for a new/updated bill of materials, RATE_REFRESH for a rate-only re-run. DLN-152/153 write this field. enum: BOM_REVISION, RATE_REFRESH |
| versionLabel | string | Human-readable label for this CA version. |
| rerunRoundCount | number | Denormalized count of with-rerun iteration rounds consumed on this chain. Defaults to 0; the feedback flow (DLN-194) increments it and enforces the round limit, rather than walking the chain on every check. |
| isInternalIteration | boolean | True when this CA version was produced by a Dalinea-internal iteration (DLN-334). Denormalized from the producing analysis-feedback's origin so the version-chain query can hide internal versions from external callers without re-deriving origin per row. Internal versions are excluded from the rerun count/cap. Defaults to false (rows predating DLN-334 and non-iteration analyses are external/customer-facing). Set only by the submit service / the orchestrator; immutable thereafter so a later PUT can't unhide/hide a version. |
| feedbackOwnerId | string | User who owns the feedback loop for this analysis (first-submitter lock). Nullable until claimed. Written only by the claim, the release, and the carry-forward that copies it onto each new version — all raw SQL, which does not pass through the immutable check. Immutable to the generated CRUD routes so an organization ADMIN cannot clear, steal or squat the claim through a PUT and bypass the release route's Dalinea-admin gate. |
| feedbackOwnerName | string | Display name of the feedback owner, captured at claim time. Nullable. Immutable to generated CRUD for the same reason as feedbackOwnerId: a name writable apart from the id it labels is the half-carry the inheritance rule already refuses. |
| reviewedById | string | User id of the person at the customer who last reviewed this version, or the version a refresh carried the review from. Nullable until reviewed. |
| reviewedByName | string | Display name of that reviewer, captured at review time. Nullable. |
| reviewedAt | string | When that review was recorded. Nullable. |
| reviewedCostAnalysisId | string | The version the reviewer actually reviewed. Equals id for a direct review; names an earlier version when a refresh carried the review forward. Nullable. |
| reviewedVersionLabel | string | Label of the reviewed version as the reviewer saw it (for example v3), kept for display. Nullable. |
| changeSummary | string | Short human-readable summary of what changed in this version. Nullable. |
| feedbackSummary | string | Short human-readable summary of the feedback that drove this version. Nullable. |
| qaVerdict | object | QA evaluation outcome. Nullable (DLN-158 writes; DLN-138 leaves null). |
| computeInputs | any | Snapshot of the run-time engine inputs NOT otherwise reconstructable from this analysis — raw financials scalars, the derived margins actually passed, and the option flags — plus the resolved tariff scalars for robust replay. Written by the orchestrator at the cost-analysis compute step (DLN-269). Makes the analysis self-describing so a recompute (iteration no-rerun, DLN-194) or audit can faithfully replay the run. Nullable on rows produced before this field existed. See ADR 0071. The financials* keys below are written on every run the orchestrator computes, carrying null when they have nothing to say — a missing VALUE is not a missing KEY, so branch on the value and never on `in`. They are absent outright only on a version carried forward from before the field existed, because a no-rerun recompute replays the prior snapshot rather than rebuilding it. |
| userTariffOverride | any | A user-pinned HTS code and/or direct tariff rate that must survive AI pipeline reruns (DLN-202). Set from an iteration_no_rerun feedback submission and copied forward to every new version in the chain. The tariff-resolution step (orchestrator Step 3d) and the recompute engine read it as a CONSTRAINT before resolving: a pinned tariffRatePct is authoritative (used instead of dataset/AI resolution), while a pinned htsCode only overrides which code is looked up (the rate is still resolved). The resulting direct tariff adjustment is emitted with source=user_provided so GET /cost-analysis/:id distinguishes it from AI-estimated values. Nullable when the user has not pinned anything. |
| buckets | CostAnalysisBucketDto[] | Rollup buckets (Direct Materials, Salaries, Import Tariff, …) derived from component results. - accepts array of full objects, id strings, or id numbers |
| id* | string | Unique identifier for the cost analysis bucket record |
| name* | string | Name of the cost component (e.g., "direct materials", "production wages", etc.) |
| category* | string | Coarse classification (e.g. 'materials', 'labor', 'overhead', 'tariff', 'profit'). |
| costAnalysis* | any | The parent cost analysis - accepts full object, id string, or id number |
| currency* | string | Currency code for the cost analysis (e.g., USD, EUR) |
| cost* | number | Cost amount for this component in minor currency units (e.g., cents) |
| costMin* | number | Minimum estimated cost for this component in minor currency units (e.g., cents) |
| costMax* | number | Maximum estimated cost for this component in minor currency units (e.g., cents) |
| presented | any | The same figures in the analysis's reportingCurrency, each round_half_up(base x fxRate). Null when the analysis reports in USD. |
| supportingData | object | Additional supporting data for this cost component as key-value pairs (e.g., dataset references, assumptions, calculation notes) |
| componentResults | CostAnalysisComponentResultDto[] | Per-BOM-component cost results (baselineCost, finalCost, stdErr). - accepts array of full objects, id strings, or id numbers |
| id* | string | Unique identifier for the component result |
| costAnalysis* | any | The parent cost analysis - accepts full object, id string, or id number |
| bomComponentId* | string | FK to BOM_COMPONENT.id. Stored as a plain uuid (no $ref) because the BOM lives in a separate standalone service — same convention as COST_ANALYSIS.productId and COST_ANALYSIS.bomId. |
| currency* | string | Currency code (e.g. USD, EUR) |
| baselineCost* | number | Pre-adjustment cost for this component, in minor currency units (cents). |
| finalCost* | number | Post-adjustment cost for this component, in minor currency units (cents). |
| stdErr | number | Approximate standard error of finalCost, in cents. Nullable (populated when Monte Carlo ran). |
| costMin | number | Lower bound (≈P10) of finalCost in minor currency units (cents). Nullable — populated only when Monte Carlo ran (same condition as stdErr). |
| costMax | number | Upper bound (≈P90) of finalCost in minor currency units (cents). Nullable — populated only when Monte Carlo ran (same condition as stdErr). |
| presented | any | The same figures in the analysis's reportingCurrency, each round_half_up(base x fxRate). Null when the analysis reports in USD. |
| rateSnapshots | CostAnalysisRateSnapshotDto[] | Frozen rate snapshots (tariff, labor, energy, localization) used in this analysis - accepts array of full objects, id strings, or id numbers |
| id* | string | Unique identifier for the rate snapshot |
| costAnalysis | any | The parent cost analysis - accepts full object, id string, or id number |
| rateType* | string | Kind of rate captured enum: tariff, labor, energy, localization, manufacturer_financials |
| layer | string | base MFN rate, or a surcharge layered on top (e.g. Section 301) enum: base, surcharge |
| source* | string | Authoritative source of the rate. USER = a user-pinned tariff (DLN-202): the pin is a recorded rate fact, emitted as a base-layer snapshot so the analysis stays reproducible (ADR 0047) and the pin is queryable. enum: USITC HTS, WITS, AI, INTERNAL, EDGAR, USER |
| sourceTable | string | Dataset table the rate was read from (e.g. usitc_hts_10digit, wits_tariff_rates) |
| sourceVersionId | string | Source dataset version stamp (e.g. "2026HTSRev8" for USITC, "2023" for WITS) |
| lookupKey* | string | The HTS/HS6 code looked up |
| value* | number | Rate as a decimal fraction (e.g. 0.075 for 7.5%) |
| unit* | string | Unit of the value. "percent" = ad-valorem percent expressed as a decimal fraction (e.g. 0.075 for 7.5%). "ratio" = dimensionless multiplier (e.g. 1.25 = +25%, 0.80 = -20%). enum: percent, ratio |
| currency | string | Currency for specific (per-unit) rates; omitted/null for ad-valorem (optional) |
| effectiveDate | string | Effective date of the rate (YYYY-MM-DD) |
| appliesToComponentName | string | BOM component this snapshot applies to (for indirect tariffs); omitted/null for the direct/product-level tariff (optional). DLN-138 maps this to the component FK at persist time. |
| componentDrivers | CostAnalysisComponentDriverDto[] | Driver-level cost attributions (tariff, labor, energy, localization, overhead, etc.) for this analysis - accepts array of full objects, id strings, or id numbers |
| id* | string | Unique identifier for the component driver |
| costAnalysis | any | The parent cost analysis - accepts full object, id string, or id number |
| driver* | string | The driver type this row attributes cost to enum: tariff, labor, energy, commodity, localization, freight, overhead, volume_discount, process_adjustment, supplier_purchasing_power |
| amount* | number | Cost impact of this driver, in minor currency units (cents). Positive means the driver increased cost; negative means it decreased it. |
| stdErr | number | Approximate standard error of `amount`, in minor currency units (cents). |
| source* | string | How `amount` was derived enum: rate_snapshot, manual_adjustment, model |
| rateSnapshotId | string | FK to COST_ANALYSIS_RATE_SNAPSHOT.id when the driver was derived from an ingested rate. Stored as a plain uuid (no $ref) following the deferred-FK convention. Nullable (overhead drivers have source=manual_adjustment with no snapshot; synthesized tariff drivers use source=model with no snapshot). |
| appliesToComponentName | string | BOM component this driver applies to, when component-specific. Absent = driver applies at the cost-analysis / product level (v1 default for labor / energy / localization / overhead). DLN-138 maps to the component FK at persist time when present. |
| metadata | object | Driver-specific qualitative context (e.g. localization sub-factor breakdown). |
| referencePrices | CostAnalysisReferencePriceDto[] | Reference prices (historical/benchmark) compared against should-cost for this analysis (DLN-189). - accepts array of full objects, id strings, or id numbers |
| id* | string | Unique identifier for the reference price |
| costAnalysis | any | The parent cost analysis this reference price was used for - accepts full object, id string, or id number |
| price* | number | Reference price in minor currency units (e.g., cents), on a PER-UNIT basis — the price of one unit of the product, never a total for the analysis's quantity. A cost analysis's shouldCostAmount, by contrast, is a total for the run's quantity, so a consumer comparing the two must scale this by that quantity (the pipeline request's `quantity`, default 1); SAVINGS_OPPORTUNITY.amount already has that scaling applied and records which basis it used in periodScope. Holding every row per-unit is what lets a row be carried over into a later analysis at a different quantity without going stale. DLN-556. |
| currency* | string | ISO 4217 code of the denomination of `price`. A price paid entered in the analysis's reportingCurrency is stored in USD at the analysis's fxRate, with the entered amount kept exactly in presented.price. On an analysis reporting in a currency other than USD, a first-hand price paid in any other currency (USD included) is stored as entered, carries no presented twin, and is never used for a savings figure; a carried-over row and an estimate are USD and are presented at the rate. |
| presented | any | The price in the analysis's reportingCurrency, present exactly for the rows the analysis can compare against: a price paid entered in that currency carries the entered amount (kept verbatim when the row is carried over to a version in the same currency); a carried-over row or an estimate in USD carries round_half_up(price x fxRate). Null when the analysis reports in USD, and for a first-hand price paid in any other currency, which is never compared. |
| pricedDate | string | Date the price was paid or observed (YYYY-MM-DD); optional |
| source* | string | How this reference price was obtained enum: user_provided, ai_estimate |
| sourceReferencePriceId | string | When this row was carried over (cloned) from a prior cost analysis in the same product lineage, the id of the reference-price row it was cloned from. Stored as a plain uuid (no $ref) following the deferred-FK convention. Null for freshly-provided or estimated references. |
| savingsOpportunities | SavingsOpportunityDto[] | Savings opportunities derived from this analysis (DLN-247). At most one per run. - accepts array of full objects, id strings, or id numbers |
| id* | string | Unique identifier for the savings opportunity |
| costAnalysis | any | The cost analysis that identified this opportunity - accepts full object, id string, or id number |
| productId* | string | Product the opportunity is for. Plain uuid (no $ref) following the deferred-FK convention (product lives in another service). |
| amount* | number | Savings amount in minor currency units (cents): reference price minus should-cost. Positive = savings; may be zero or negative when the should-cost is at or above the reference price. |
| currency* | string | ISO 4217 code of the denomination of `amount`: the analysis's base currency (USD), the same denomination as both operands of the gap. |
| presented | any | The gap in the analysis's reportingCurrency, taken between the presented reference price and the presented should-cost, so both operands are in one denomination. NOT `amount` x fxRate, and it does not always equal it: a reference price entered in the reporting currency keeps the typed amount exactly and derives its base, so the two columns differ by the rate applied to that half-cent of rounding — under a minor unit at a rate near 1, and tens of them at a rate in the hundreds. Join on one column or the other, never across the pair. Null when the analysis reports in USD. |
| lowestPriceGap | number | On an analysis priced from the product's purchases (ADR 0156): the lowest price paid per negotiation unit minus the should-cost, in minor units of `currency`. Null otherwise, and when the purchases have no lowest price. |
| highestPriceGap | number | On an analysis priced from the product's purchases: the highest price paid per negotiation unit minus the should-cost, in minor units of `currency`. Null otherwise, and when the purchases have no highest price. |
| quantityBought | number | On an analysis priced from the product's purchases: the negotiation units bought in the roll-up's period, unrounded (800 each is 66.67 packs of 12). Null otherwise. |
| savingsAtStake | number | On an analysis priced from the product's purchases: the savings at stake, `amount` (the gap per negotiation unit at the average price) × quantityBought, rounded half up, in minor units of `currency`. Null otherwise. |
| periodScope* | string | Basis the amount is expressed on enum: per_unit, annual, total |
| status* | string | Lifecycle status; opportunities are created "open" enum: open, closed |
| identifiedDate* | string | Date the opportunity was identified (the cost analysis date, YYYY-MM-DD) |
| comparisonBasis* | string | What the savings is measured against enum: prices_paid, prior_analysis, external_benchmark, target_cost |
| comparisonReferenceId* | string | Id of the COST_ANALYSIS_REFERENCE_PRICE row this opportunity was computed against. Plain string (no $ref) per the ERD's non-polymorphic-FK convention. |
| assigneeUserId | string | User the opportunity is assigned to (deferred-FK to the user service). Not populated by the pipeline; set by a later workflow. |
| targetCloseDate | string | Target date to act on the opportunity. Populated by a later workflow. |
| notes | string | Free-text notes. Populated by a later workflow. |
| adjustments | AdjustmentsDto[] | Adjustments applied to the cost model beyond the industry-average ASM baseline. Records which factors shaped the analysis without capturing exact numeric deltas. |
| type* | string | Identifier for the kind of adjustment applied enum: manufacturer_financials, yield_adjustment, compliance_overhead, setup_amortization, named_adjustment, tariff, indirect_tariff, volume_discount, supplier_purchasing_power |
| source* | string | Origin of this adjustment enum: user_provided, ai_estimate, analyst_judgment, system, edgar_auto, operator_entry |
| label | string | Human-readable description of what was adjusted |
| metadata | object | Adjustment-specific qualitative context (no dollar amounts). For type="manufacturer_financials": may include - cogsRatioAdjusted (boolean) - profitMarginOverridden (boolean) - fiscalYear (integer): the FY the financials describe - companyFinancialsId (uuid): the specific company_financials row that fed this run, enabling audit even after the cache rolls to a newer filing - financialsSource ("edgar" | "manual" | "llm" | "dto"): where the financials came from for this run - sgaCompleteness ({found:[...], missing:[...]}): presence of each financial figure the SG&A selection reads — revenue, cogs, the sm/ga pair, R&D, and the combined SG&A line — plus the reported operating income, tracked here as drift-detection evidence even though DLN-913 margin derivation never reads it: the operating margin comes only from revenue, cogs and SG&A. `missing` is not a defect signal: a filer that legitimately never files a given tag (e.g. a decomposed filer with no combined line) reports it missing without anything being wrong. - operatingMarginSource ("edgar_sga" | "asm_industry_average"): which route produced the margin PAIR. "edgar_sga" — the manufacturer's own margins, the operating half reconstructed from an SG&A sum: `(revenue - cogs - sga) / revenue` (the name is a misnomer for the manual-DTO case too; kept for backward compatibility). This is the ONLY way a manufacturer's own operating margin can be adopted. DLN-913 reversed an earlier decision (PR #636) to prefer a filer's own reported `us-gaap:OperatingIncomeLoss`: we cannot know what a filer included when it calculated that figure, so we derive the margin ourselves and never adopt theirs, however it compares to our own gross margin. "edgar_operating_income" was that now-retired source value; it can no longer appear. "asm_industry_average" — BOTH margins came from the industry baseline. Since DLN-913 the manufacturer's gross and operating margin are adopted together or not at all, so this value means the whole pair is industry-sourced; before DLN-913 a run could carry the manufacturer's gross margin beside an industry or derived operating margin. - industryAverageReason ("no_margin_pair" | "implausible_gross_margin"): why the run fell back. Present only when operatingMarginSource is "asm_industry_average". "no_margin_pair" — financials reached the engine (at least one of COGS, SG&A or the GROSS margin was supplied — an operating margin alone does not emit this adjustment) but did not yield both margins, so neither was adopted; this covers a half pair whose present half is the gross margin, and a filing that resolved some figure but neither margin (e.g. SG&A with no revenue). "implausible_gross_margin" — the DLN-888 band withheld the whole set. A run with NO manufacturer financials at all (nothing supplied) emits no manufacturer_financials adjustment and so carries neither this key nor operatingMarginSource — the frontend's own `financialsStatus`/`financialsSource` fields on computeInputs are what explain that case, not this metadata; so does a run whose only supplied figure is an operating margin — absence of the adjustment does not by itself mean nothing was supplied. - llmCitations ([{field, url, note}]): present only when financialsSource == "llm" For type="tariff" (direct product-level import tariff): may include - importCountry (string) - countryOfOrigin (string) - ratePercent (number) For type="indirect_tariff" (tariffs on foreign-sourced BOM components): may include - components (object): per-component breakdown of the indirect tariff impact |
| meta | object |
User
User identity and profile records. Each user maps 1:1 to a Firebase Auth account.
/users/{id}
Get user by ID (scoped to caller membership)
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required |
Responses
| Field | Type | Description |
|---|---|---|
| data | UserDto | |
| id* | string | Unique identifier for the user |
| firebaseUid | string | Firebase Auth UID — links this record to the user's GCIP auth account. Unique across all non-null values. Server-set: for INTERNAL users, User.AfterCreate calls tenant.createUser() and writes the returned uid back to this row; for EXTERNAL (invited) users the uid is bound at first SSO sign-in via resolve-invite (DLN-266) and starts as null. Any value supplied on POST is rejected. Immutable after creation. Uniqueness on non-null values is enforced by a partial unique index (not a Cruddy-managed constraint) that the user service installs at boot; see services/user/src/bootstrap/install-unique-indexes.ts. |
| email* | string | User's email address. Unique across all users. |
| firstName* | string | User's first name |
| lastName* | string | User's last name |
| displayName* | string | Display name shown in the UI (may differ from firstName + lastName) |
| image | string | GCS filename for the user's profile picture |
| title | string | Job title or role at the user's organization |
| phone | string | Contact phone number |
| bio | string | Short biography or about text |
| userType* | string | Whether this user is a Dalinea employee (INTERNAL) or a client user (EXTERNAL). Immutable after creation. enum: INTERNAL, EXTERNAL |
| status* | string | Lifecycle status of the user account enum: ACTIVE, INACTIVE, SUSPENDED, DELETED |
| language* | string | Preferred language (ISO 639-1 two-character code, e.g. "en") |
| lastLogin | string | Timestamp of the user's most recent login |
| firebaseTenantId | string | GCIP tenant ID for the organization this user authenticates under. Present for tenant-scoped users (EXTERNAL, DALINEA_ANALYST); absent for platform-scoped INTERNAL users (DALINEA_ADMIN) who authenticate at the project level. Determines which GCIP tenant createUser targets. |
| meta | object |
/users/{id}
Update user (scoped to caller membership)
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required |
Request Body
| Field | Type | Description |
|---|---|---|
| id* | string | Unique identifier for the user |
| firebaseUid | string | Firebase Auth UID — links this record to the user's GCIP auth account. Unique across all non-null values. Server-set: for INTERNAL users, User.AfterCreate calls tenant.createUser() and writes the returned uid back to this row; for EXTERNAL (invited) users the uid is bound at first SSO sign-in via resolve-invite (DLN-266) and starts as null. Any value supplied on POST is rejected. Immutable after creation. Uniqueness on non-null values is enforced by a partial unique index (not a Cruddy-managed constraint) that the user service installs at boot; see services/user/src/bootstrap/install-unique-indexes.ts. |
| email* | string | User's email address. Unique across all users. |
| firstName* | string | User's first name |
| lastName* | string | User's last name |
| displayName* | string | Display name shown in the UI (may differ from firstName + lastName) |
| image | string | GCS filename for the user's profile picture |
| title | string | Job title or role at the user's organization |
| phone | string | Contact phone number |
| bio | string | Short biography or about text |
| userType* | string | Whether this user is a Dalinea employee (INTERNAL) or a client user (EXTERNAL). Immutable after creation. enum: INTERNAL, EXTERNAL |
| status* | string | Lifecycle status of the user account enum: ACTIVE, INACTIVE, SUSPENDED, DELETED |
| language* | string | Preferred language (ISO 639-1 two-character code, e.g. "en") |
| lastLogin | string | Timestamp of the user's most recent login |
| firebaseTenantId | string | GCIP tenant ID for the organization this user authenticates under. Present for tenant-scoped users (EXTERNAL, DALINEA_ANALYST); absent for platform-scoped INTERNAL users (DALINEA_ADMIN) who authenticate at the project level. Determines which GCIP tenant createUser targets. |
Responses
| Field | Type | Description |
|---|---|---|
| data | UserDto | |
| id* | string | Unique identifier for the user |
| firebaseUid | string | Firebase Auth UID — links this record to the user's GCIP auth account. Unique across all non-null values. Server-set: for INTERNAL users, User.AfterCreate calls tenant.createUser() and writes the returned uid back to this row; for EXTERNAL (invited) users the uid is bound at first SSO sign-in via resolve-invite (DLN-266) and starts as null. Any value supplied on POST is rejected. Immutable after creation. Uniqueness on non-null values is enforced by a partial unique index (not a Cruddy-managed constraint) that the user service installs at boot; see services/user/src/bootstrap/install-unique-indexes.ts. |
| email* | string | User's email address. Unique across all users. |
| firstName* | string | User's first name |
| lastName* | string | User's last name |
| displayName* | string | Display name shown in the UI (may differ from firstName + lastName) |
| image | string | GCS filename for the user's profile picture |
| title | string | Job title or role at the user's organization |
| phone | string | Contact phone number |
| bio | string | Short biography or about text |
| userType* | string | Whether this user is a Dalinea employee (INTERNAL) or a client user (EXTERNAL). Immutable after creation. enum: INTERNAL, EXTERNAL |
| status* | string | Lifecycle status of the user account enum: ACTIVE, INACTIVE, SUSPENDED, DELETED |
| language* | string | Preferred language (ISO 639-1 two-character code, e.g. "en") |
| lastLogin | string | Timestamp of the user's most recent login |
| firebaseTenantId | string | GCIP tenant ID for the organization this user authenticates under. Present for tenant-scoped users (EXTERNAL, DALINEA_ANALYST); absent for platform-scoped INTERNAL users (DALINEA_ADMIN) who authenticate at the project level. Determines which GCIP tenant createUser targets. |
| meta | object |
Organization
Tenant organizations. Each owns a GCIP tenant and one or more email domains used for sign-in discovery.
/organizations/{id}
Get organization by ID (scoped to caller membership)
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required |
Responses
| Field | Type | Description |
|---|---|---|
| data | OrganizationDto | |
| id* | string | Unique identifier for the organization |
| name* | string | Display name of the organization |
| status* | string | Lifecycle status of the organization enum: ACTIVE, ARCHIVED, SUSPENDED |
| legalEntityName | string | Full legal entity name (may differ from display name) |
| taxId | string | Tax identification number (EIN, VAT, etc.) |
| crmId | string | CRM system identifier for this organization |
| companyId | string | FK to the @company record linked to this organization |
| defaultCurrency | string | Default ISO 4217 currency code (e.g. USD, EUR) |
| fiscalYearStart* | number | Month in which the fiscal year starts (1 = January, 12 = December) |
| redMultiplier | number | Signal's RED threshold as a markup tolerance over the band (DLN-894): an item is RED when overRatio (paid / band high) exceeds it, GREEN otherwise. 1 is strict, and is what a stored null reads as. Changed only through PUT /organizations/:id/signal-threshold, by Dalinea staff; the generic update rejects it. Withheld from the warehouse: the verdict routes read it from this service per request (DLN-960), never from the replicated table. |
| dateFormat* | string | Preferred date display format for this organization enum: MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD |
| defaultTimeZone* | string | Default time zone identifier for this organization (e.g. America/New_York, America/Chicago, Europe/Berlin, Asia/Tokyo) |
| domains* | string[] | Email domains associated with this organization (e.g. ["acme.com", "acme-legacy.com"]). Used for tenant discovery. Must be unique across all organizations. |
| firebaseTenantId | string | Google Cloud Identity Platform (GCIP) tenant ID for this organization. Auto-generated by GCIP (not a UUID). Set after org creation by the Firebase Admin SDK event listener. Required for EXTERNAL user authentication. |
| microsoftTenantId | string | Azure AD (Microsoft Entra) directory/tenant ID for this organization's customer-managed, single-tenant Microsoft SSO app registration. When set, the frontend passes it as the Microsoft OAuth `tenant` custom parameter so sign-in targets the customer's tenant authority instead of the default `common` (which single-tenant apps reject, AADSTS50194). Leave unset for orgs on the shared multi-tenant Dalinea app. |
| meta | object |
/organizations
List organization documents
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| microsoftTenantId | query | string[] | optional | Filter by a set of values (repeatable param) |
| firebaseTenantId | query | string[] | optional | Filter by a set of values (repeatable param) |
| defaultTimeZone | query | string[] | optional | Filter by a set of values (repeatable param) |
| dateFormat | query | string[] | optional | Filter by a set of values (repeatable param) |
| fiscalYearStart | query | string[] | optional | Filter by a set of values (repeatable param) |
| defaultCurrency | query | string[] | optional | Filter by a set of values (repeatable param) |
| companyId | query | string[] | optional | Filter by a set of values (repeatable param) |
| crmId | query | string[] | optional | Filter by a set of values (repeatable param) |
| taxId | query | string[] | optional | Filter by a set of values (repeatable param) |
| legalEntityName | query | string[] | optional | Filter by a set of values (repeatable param) |
| status | query | string[] | optional | Filter by a set of values (repeatable param) |
| name | query | string[] | optional | Filter by a set of values (repeatable param) |
| id | query | string[] | optional | Filter by a set of values (repeatable param) |
| microsoftTenantId[contains] | query | string | optional | Case-insensitive substring match |
| firebaseTenantId[contains] | query | string | optional | Case-insensitive substring match |
| defaultTimeZone[contains] | query | string | optional | Case-insensitive substring match |
| dateFormat[contains] | query | string | optional | Case-insensitive substring match |
| defaultCurrency[contains] | query | string | optional | Case-insensitive substring match |
| crmId[contains] | query | string | optional | Case-insensitive substring match |
| taxId[contains] | query | string | optional | Case-insensitive substring match |
| legalEntityName[contains] | query | string | optional | Case-insensitive substring match |
| status[contains] | query | string | optional | Case-insensitive substring match |
| name[contains] | query | string | optional | Case-insensitive substring match |
| count | query | boolean | optional | Include totalCount of unbounded results in response meta |
| related | query | string[] | optional | Include related entities. Omit for all, "false" for none, or specify field names (repeatable, supports dot notation) |
| sortOrder | query | string | optional | Sort direction (default: asc) |
| sortBy | query | string | optional | Field name to sort by |
| offset | query | number | optional | |
| limit | query | number | optional |
Responses
| Field | Type | Description |
|---|---|---|
| data | OrganizationDto[] | |
| id* | string | Unique identifier for the organization |
| name* | string | Display name of the organization |
| status* | string | Lifecycle status of the organization enum: ACTIVE, ARCHIVED, SUSPENDED |
| legalEntityName | string | Full legal entity name (may differ from display name) |
| taxId | string | Tax identification number (EIN, VAT, etc.) |
| crmId | string | CRM system identifier for this organization |
| companyId | string | FK to the @company record linked to this organization |
| defaultCurrency | string | Default ISO 4217 currency code (e.g. USD, EUR) |
| fiscalYearStart* | number | Month in which the fiscal year starts (1 = January, 12 = December) |
| redMultiplier | number | Signal's RED threshold as a markup tolerance over the band (DLN-894): an item is RED when overRatio (paid / band high) exceeds it, GREEN otherwise. 1 is strict, and is what a stored null reads as. Changed only through PUT /organizations/:id/signal-threshold, by Dalinea staff; the generic update rejects it. Withheld from the warehouse: the verdict routes read it from this service per request (DLN-960), never from the replicated table. |
| dateFormat* | string | Preferred date display format for this organization enum: MM/DD/YYYY, DD/MM/YYYY, YYYY-MM-DD |
| defaultTimeZone* | string | Default time zone identifier for this organization (e.g. America/New_York, America/Chicago, Europe/Berlin, Asia/Tokyo) |
| domains* | string[] | Email domains associated with this organization (e.g. ["acme.com", "acme-legacy.com"]). Used for tenant discovery. Must be unique across all organizations. |
| firebaseTenantId | string | Google Cloud Identity Platform (GCIP) tenant ID for this organization. Auto-generated by GCIP (not a UUID). Set after org creation by the Firebase Admin SDK event listener. Required for EXTERNAL user authentication. |
| microsoftTenantId | string | Azure AD (Microsoft Entra) directory/tenant ID for this organization's customer-managed, single-tenant Microsoft SSO app registration. When set, the frontend passes it as the Microsoft OAuth `tenant` custom parameter so sign-in targets the customer's tenant authority instead of the default `common` (which single-tenant apps reject, AADSTS50194). Leave unset for orgs on the shared multi-tenant Dalinea app. |
| meta | object | |
| totalCount | integer | Unbounded result count. Only present when ?count=true is passed. |
Feature Flags
Feature flag configuration and resolution.
/feature-flags/defaults
Get the feature flag registry with the defaults this environment serves
Every declared flag with the default an organization with no override row gets in this environment, and the metadata needed to present it: label, kind (rollout vs restriction), and any flags it requires to take effect. `description` is operator-facing and is served only to Dalinea-internal callers. Resolution endpoints return the flat name-to-boolean map for a specific organization or user instead.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| x-user-type | header | string | optional | Gateway-set INTERNAL/EXTERNAL user type. Server-injected — clients never supply it; the gateway strips inbound copies. Absent → treated as external, so `description` is withheld. |
Responses
| Field | Type | Description |
|---|---|---|
| data* | object | Flag name to its registry entry. |
| meta* | object | |
| type* | string | enum: FeatureFlagDefaults |
Imports
Upload files for sharing or to feed into batch workflows like cost analysis.
/imports/upload-url
Get a signed PUT URL for uploading an import file.
Validates the asserted MIME type against the registry for the given importType. Returns a fileLocation the client must echo back to POST /imports once the upload is complete.
Request Body
| Field | Type | Description |
|---|---|---|
| originalFilename* | string | Original filename (used as the leaf of the object path). |
| mimeType* | string | Client-asserted MIME type. Validated against the requested import type's allowlist. |
| sizeBytes* | number | Client-asserted file size in bytes. Rejected if FMS limit exceeded. |
| importType | string | Import type the file is for. Picks the MIME allowlist and the upload size cap (DLN-920). Defaults to COST_ANALYSIS_IMPORT. enum: COST_ANALYSIS_IMPORT, SIGNAL_IMPORT, TRANSACTION_IMPORT |
Responses
| Field | Type | Description |
|---|---|---|
| data | ImportUploadUrlResponseDto | |
| uploadUrl* | string | |
| fileLocation* | string | GCS path of the form {bucket}/{tenantId}/{uuid}/{filename}. |
| expiresAt* | string | |
| contentType* | string | MIME type the PUT must declare (echoed as Content-Type). |
| requiredHeaders* | object | Headers the client MUST send verbatim on the PUT for the V4 signature to validate. Always includes content-type; includes x-goog-content-length-range when a size cap is set. Send exactly these — omitting one yields a 403 SignatureDoesNotMatch. |
| meta | object |
/imports/preview
Preview the columns + first few rows of an uploaded file.
For workflow imports the UI uses this to build column mappings. Streams the file through FMS, parses up to maxRows rows, and returns them.
Request Body
| Field | Type | Description |
|---|---|---|
| fileLocation* | string | fileLocation returned from upload-url, of the form {bucket}/{tenantId}/{uuid}/{filename}. |
| maxRows | number | Max sample rows to return. Default 5, max 50. |
Responses
| Field | Type | Description |
|---|---|---|
| data | PreviewResponseDto | |
| columns* | string[] | |
| sampleRows* | object[] | |
| totalRowsEstimate | number | Best-effort total-rows count, when the parser can compute it. |
| meta | object |
/imports
List import documents
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| failedRowCount | query | string[] | optional | Filter by a set of values (repeatable param) |
| productJobsSubmittedAt | query | string[] | optional | Filter by a set of values (repeatable param) |
| processedRowCount | query | string[] | optional | Filter by a set of values (repeatable param) |
| totalRowCount | query | string[] | optional | Filter by a set of values (repeatable param) |
| errorMessage | query | string[] | optional | Filter by a set of values (repeatable param) |
| rejectionReason | query | string[] | optional | Filter by a set of values (repeatable param) |
| approvalMode | query | string[] | optional | Filter by a set of values (repeatable param) |
| approvedAt | query | string[] | optional | Filter by a set of values (repeatable param) |
| approvedByUserId | query | string[] | optional | Filter by a set of values (repeatable param) |
| uploadedByUserId | query | string[] | optional | Filter by a set of values (repeatable param) |
| status | query | string[] | optional | Filter by a set of values (repeatable param) |
| type | query | string[] | optional | Filter by a set of values (repeatable param) |
| sizeBytes | query | string[] | optional | Filter by a set of values (repeatable param) |
| mimeType | query | string[] | optional | Filter by a set of values (repeatable param) |
| originalFilename | query | string[] | optional | Filter by a set of values (repeatable param) |
| fileLocation | query | string[] | optional | Filter by a set of values (repeatable param) |
| description | query | string[] | optional | Filter by a set of values (repeatable param) |
| name | query | string[] | optional | Filter by a set of values (repeatable param) |
| id | query | string[] | optional | Filter by a set of values (repeatable param) |
| errorMessage[contains] | query | string | optional | Case-insensitive substring match |
| rejectionReason[contains] | query | string | optional | Case-insensitive substring match |
| approvalMode[contains] | query | string | optional | Case-insensitive substring match |
| approvedByUserId[contains] | query | string | optional | Case-insensitive substring match |
| uploadedByUserId[contains] | query | string | optional | Case-insensitive substring match |
| status[contains] | query | string | optional | Case-insensitive substring match |
| type[contains] | query | string | optional | Case-insensitive substring match |
| mimeType[contains] | query | string | optional | Case-insensitive substring match |
| originalFilename[contains] | query | string | optional | Case-insensitive substring match |
| fileLocation[contains] | query | string | optional | Case-insensitive substring match |
| description[contains] | query | string | optional | Case-insensitive substring match |
| name[contains] | query | string | optional | Case-insensitive substring match |
| count | query | boolean | optional | Include totalCount of unbounded results in response meta |
| related | query | string[] | optional | Include related entities. Omit for all, "false" for none, or specify field names (repeatable, supports dot notation) |
| sortOrder | query | string | optional | Sort direction (default: asc) |
| sortBy | query | string | optional | Field name to sort by |
| offset | query | number | optional | |
| limit | query | number | optional |
Responses
| Field | Type | Description |
|---|---|---|
| data | ImportDto[] | |
| id | string | Unique identifier for the import |
| name* | string | User-supplied display name for the import |
| description | string | Optional user-supplied description |
| fileLocation* | string | GCS path of the form "{bucket}/{tenantId}/{uuid}/{filename}" |
| originalFilename* | string | Filename as supplied by the user at upload time |
| mimeType* | string | Actual MIME type reported by GCS at create time |
| sizeBytes* | number | Actual size in bytes reported by GCS at create time |
| type* | string | What kind of import this is, which decides how its rows are used. COST_ANALYSIS_IMPORT is IMPACT, the should-cost/BOM pipeline, and the default for every caller that does not ask. SIGNAL_IMPORT is the deterministic paid-price-vs-band screen (DLN-503), available only to organizations holding the SIGNAL_ANALYSIS flag. TRANSACTION_IMPORT is a file of purchase lines (invoice or PO) mapped for the SIGNAL or IMPACT bulk route, available only to organizations holding the TRANSACTIONS flag. See src/imports/import-types.ts for the registry. enum: COST_ANALYSIS_IMPORT, SIGNAL_IMPORT, TRANSACTION_IMPORT |
| status* | string | Import lifecycle status. PENDING_CONFIG until the mapping is complete. A valid mapping moves a TRANSACTION_IMPORT to SUMMARY_READY, where its transactions are stored and nothing runs, until the uploader submits it; every other type goes straight to PENDING_APPROVAL. A transaction import on the SIGNAL route that an admin approves is PROCESSING ("Screening") until the last of its products is screened, then COMPLETED, PARTIALLY_COMPLETED or FAILED (ADR 0154); one on the IMPACT route is APPROVED and runs nothing yet. Every other approved type is PROCESSING. REJECTED is final. See ADR 0149. enum: PENDING_CONFIG, SUMMARY_READY, PENDING_APPROVAL, APPROVED, PROCESSING, COMPLETED, PARTIALLY_COMPLETED, FAILED, REJECTED |
| uploadedByUserId* | string | ID of the user that uploaded this import (from the request's auth context) |
| approvedByUserId | string | ID of the user who approved this import (set on approval): a Dalinea admin, or a client admin or owner. Unset for an AUTOMATIC dispatch. |
| approvedAt | string | Timestamp the import was approved |
| approvalMode | string | How the import was approved: INTERNAL (by Dalinea staff), EXTERNAL (by a client admin or owner, under the import-approvals flag for a type that runs the processor, or for any transaction import), or AUTOMATIC (dispatched by the system with no user action at all; never a transaction import). Unset while the import is still awaiting approval. The import-processor reads this to pick a row cap — anything that ran without Dalinea review is held to the tighter unattended limit. enum: INTERNAL, EXTERNAL, AUTOMATIC |
| rejectionReason | string | The reason the admin gave when rejecting the import, shown to the uploader. Optional; unset when the admin gave none or the import was not rejected. |
| config | object | Type-specific configuration. Validated by the service layer per type. |
| errorMessage | string | Error message if the import failed during processing |
| totalRowCount | number | Total rows the processor identified in the file |
| processedRowCount | number | Number of rows the processor has finished (successfully or otherwise) |
| productJobsSubmittedAt | string | When the import processor finished submitting an approved IMPACT bulk transaction import's runs, one per product (ADR 0162). Null until then, and cleared on each approval. The import does not end before it is set. |
| failedRowCount | number | Number of rows that failed processing |
| meta | object | |
| totalCount | integer | Unbounded result count. Only present when ?count=true is passed. |
/imports
Create an Import record from an uploaded file.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| x-feature-flags | header | string | optional | Gateway-resolved feature flags for this (organization, user). Server-injected — clients never supply it; the gateway strips inbound copies. Consulted when `type` is SIGNAL_IMPORT: absent or missing SIGNAL_ANALYSIS is a 403, for internal and external callers alike. Likewise TRANSACTIONS when `type` is TRANSACTION_IMPORT. |
Request Body
| Field | Type | Description |
|---|---|---|
| name* | string | |
| description | string | |
| fileLocation* | string | fileLocation returned from POST /imports/upload-url. |
| originalFilename* | string | |
| type | string | Which analysis framework this import runs under. Defaults to COST_ANALYSIS_IMPORT when omitted — every existing caller is unaffected. SIGNAL_IMPORT requires the organization to hold the SIGNAL_ANALYSIS flag; requesting it otherwise is a 403 (a capability gap, not a malformed request), for internal and external callers alike (SIGNAL_ANALYSIS is a rollout flag, not a restriction flag — see import-types.ts). TRANSACTION_IMPORT requires the TRANSACTIONS flag in the same way. enum: COST_ANALYSIS_IMPORT, SIGNAL_IMPORT, TRANSACTION_IMPORT |
| config | object | Type-specific configuration, validated against `type`'s DTO (CostAnalysisImportConfig when `type` is omitted, SignalImportConfig for SIGNAL_IMPORT, TransactionImportConfig for TRANSACTION_IMPORT). A missing or incomplete columnMappings leaves the import PENDING_CONFIG; any other invalid value is refused with 400. A TRANSACTION_IMPORT config names its `route` (SIGNAL_2A or IMPACT_1B), sent here or with the first mapping; a missing or unknown route is refused with 400. `spendCubeExport: true` (default false) marks a file with no document key: document number and line number are then recommended, not required. A TRANSACTION_IMPORT mapping that leaves out a column its route requires is refused with 400 naming each missing column; any other incomplete or wrong-typed mapping parks it in PENDING_CONFIG. A TRANSACTION_IMPORT columnMappings may also carry `customFields`: custom field id (GET /custom-field) to column header, at most 50, each field once. A custom field never fills a required column. An id the organisation has no live custom field for passes this check, but the ingest that follows a valid mapping refuses it: the import records the error and stores no transactions. |
Responses
| Field | Type | Description |
|---|---|---|
| data | ImportDto | |
| id | string | Unique identifier for the import |
| name* | string | User-supplied display name for the import |
| description | string | Optional user-supplied description |
| fileLocation* | string | GCS path of the form "{bucket}/{tenantId}/{uuid}/{filename}" |
| originalFilename* | string | Filename as supplied by the user at upload time |
| mimeType* | string | Actual MIME type reported by GCS at create time |
| sizeBytes* | number | Actual size in bytes reported by GCS at create time |
| type* | string | What kind of import this is, which decides how its rows are used. COST_ANALYSIS_IMPORT is IMPACT, the should-cost/BOM pipeline, and the default for every caller that does not ask. SIGNAL_IMPORT is the deterministic paid-price-vs-band screen (DLN-503), available only to organizations holding the SIGNAL_ANALYSIS flag. TRANSACTION_IMPORT is a file of purchase lines (invoice or PO) mapped for the SIGNAL or IMPACT bulk route, available only to organizations holding the TRANSACTIONS flag. See src/imports/import-types.ts for the registry. enum: COST_ANALYSIS_IMPORT, SIGNAL_IMPORT, TRANSACTION_IMPORT |
| status* | string | Import lifecycle status. PENDING_CONFIG until the mapping is complete. A valid mapping moves a TRANSACTION_IMPORT to SUMMARY_READY, where its transactions are stored and nothing runs, until the uploader submits it; every other type goes straight to PENDING_APPROVAL. A transaction import on the SIGNAL route that an admin approves is PROCESSING ("Screening") until the last of its products is screened, then COMPLETED, PARTIALLY_COMPLETED or FAILED (ADR 0154); one on the IMPACT route is APPROVED and runs nothing yet. Every other approved type is PROCESSING. REJECTED is final. See ADR 0149. enum: PENDING_CONFIG, SUMMARY_READY, PENDING_APPROVAL, APPROVED, PROCESSING, COMPLETED, PARTIALLY_COMPLETED, FAILED, REJECTED |
| uploadedByUserId* | string | ID of the user that uploaded this import (from the request's auth context) |
| approvedByUserId | string | ID of the user who approved this import (set on approval): a Dalinea admin, or a client admin or owner. Unset for an AUTOMATIC dispatch. |
| approvedAt | string | Timestamp the import was approved |
| approvalMode | string | How the import was approved: INTERNAL (by Dalinea staff), EXTERNAL (by a client admin or owner, under the import-approvals flag for a type that runs the processor, or for any transaction import), or AUTOMATIC (dispatched by the system with no user action at all; never a transaction import). Unset while the import is still awaiting approval. The import-processor reads this to pick a row cap — anything that ran without Dalinea review is held to the tighter unattended limit. enum: INTERNAL, EXTERNAL, AUTOMATIC |
| rejectionReason | string | The reason the admin gave when rejecting the import, shown to the uploader. Optional; unset when the admin gave none or the import was not rejected. |
| config | object | Type-specific configuration. Validated by the service layer per type. |
| errorMessage | string | Error message if the import failed during processing |
| totalRowCount | number | Total rows the processor identified in the file |
| processedRowCount | number | Number of rows the processor has finished (successfully or otherwise) |
| productJobsSubmittedAt | string | When the import processor finished submitting an approved IMPACT bulk transaction import's runs, one per product (ADR 0162). Null until then, and cleared on each approval. The import does not end before it is set. |
| failedRowCount | number | Number of rows that failed processing |
| meta | object |
/imports/{id}
Get import by ID
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required | |
| related | query | string[] | optional | Include related entities. Omit for all, "false" for none, or specify field names (repeatable, supports dot notation) |
Responses
| Field | Type | Description |
|---|---|---|
| data | ImportDto | |
| id | string | Unique identifier for the import |
| name* | string | User-supplied display name for the import |
| description | string | Optional user-supplied description |
| fileLocation* | string | GCS path of the form "{bucket}/{tenantId}/{uuid}/{filename}" |
| originalFilename* | string | Filename as supplied by the user at upload time |
| mimeType* | string | Actual MIME type reported by GCS at create time |
| sizeBytes* | number | Actual size in bytes reported by GCS at create time |
| type* | string | What kind of import this is, which decides how its rows are used. COST_ANALYSIS_IMPORT is IMPACT, the should-cost/BOM pipeline, and the default for every caller that does not ask. SIGNAL_IMPORT is the deterministic paid-price-vs-band screen (DLN-503), available only to organizations holding the SIGNAL_ANALYSIS flag. TRANSACTION_IMPORT is a file of purchase lines (invoice or PO) mapped for the SIGNAL or IMPACT bulk route, available only to organizations holding the TRANSACTIONS flag. See src/imports/import-types.ts for the registry. enum: COST_ANALYSIS_IMPORT, SIGNAL_IMPORT, TRANSACTION_IMPORT |
| status* | string | Import lifecycle status. PENDING_CONFIG until the mapping is complete. A valid mapping moves a TRANSACTION_IMPORT to SUMMARY_READY, where its transactions are stored and nothing runs, until the uploader submits it; every other type goes straight to PENDING_APPROVAL. A transaction import on the SIGNAL route that an admin approves is PROCESSING ("Screening") until the last of its products is screened, then COMPLETED, PARTIALLY_COMPLETED or FAILED (ADR 0154); one on the IMPACT route is APPROVED and runs nothing yet. Every other approved type is PROCESSING. REJECTED is final. See ADR 0149. enum: PENDING_CONFIG, SUMMARY_READY, PENDING_APPROVAL, APPROVED, PROCESSING, COMPLETED, PARTIALLY_COMPLETED, FAILED, REJECTED |
| uploadedByUserId* | string | ID of the user that uploaded this import (from the request's auth context) |
| approvedByUserId | string | ID of the user who approved this import (set on approval): a Dalinea admin, or a client admin or owner. Unset for an AUTOMATIC dispatch. |
| approvedAt | string | Timestamp the import was approved |
| approvalMode | string | How the import was approved: INTERNAL (by Dalinea staff), EXTERNAL (by a client admin or owner, under the import-approvals flag for a type that runs the processor, or for any transaction import), or AUTOMATIC (dispatched by the system with no user action at all; never a transaction import). Unset while the import is still awaiting approval. The import-processor reads this to pick a row cap — anything that ran without Dalinea review is held to the tighter unattended limit. enum: INTERNAL, EXTERNAL, AUTOMATIC |
| rejectionReason | string | The reason the admin gave when rejecting the import, shown to the uploader. Optional; unset when the admin gave none or the import was not rejected. |
| config | object | Type-specific configuration. Validated by the service layer per type. |
| errorMessage | string | Error message if the import failed during processing |
| totalRowCount | number | Total rows the processor identified in the file |
| processedRowCount | number | Number of rows the processor has finished (successfully or otherwise) |
| productJobsSubmittedAt | string | When the import processor finished submitting an approved IMPACT bulk transaction import's runs, one per product (ADR 0162). Null until then, and cleared on each approval. The import does not end before it is set. |
| failedRowCount | number | Number of rows that failed processing |
| meta | object |
/imports/{id}
Partial-update name/description/config. Recomputes status from config.
Allowed only while the import is PENDING_CONFIG, SUMMARY_READY or PENDING_APPROVAL. config is merged onto the stored config one top-level key at a time: a key the body names (columnMappings, jobDefaults) replaces the stored value whole, and a key it omits is kept. Status is then recomputed: valid → PENDING_APPROVAL (SUMMARY_READY for a TRANSACTION_IMPORT, which stores its transactions and waits for POST /imports/{id}/submit), a missing or incomplete columnMappings → PENDING_CONFIG. A malformed value outside columnMappings, or a key the config does not declare, is refused with 400. So is a TRANSACTION_IMPORT columnMappings that leaves out a column its route requires: the response names each missing column.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required | |
| x-feature-flags | header | string | optional | Gateway-resolved feature flags for this (organization, user). Consulted when `type` changes to SIGNAL_IMPORT or TRANSACTION_IMPORT — see POST /imports. |
Request Body
| Field | Type | Description |
|---|---|---|
| name | string | |
| description | string | |
| type | string | Change the analysis framework this import runs under. Only honored while the import is PENDING_CONFIG — any other status is a 409. Revalidates `config` against the new type's DTO and recomputes status; the stored config is NOT carried across a type change, so a patch that changes type without also sending config clears it and the import returns to PENDING_CONFIG to be re-mapped. SIGNAL_IMPORT requires the organization to hold the SIGNAL_ANALYSIS flag; requesting it otherwise is a 403 (a capability gap, not a malformed request), for internal and external callers alike. TRANSACTION_IMPORT requires the TRANSACTIONS flag in the same way. enum: COST_ANALYSIS_IMPORT, SIGNAL_IMPORT, TRANSACTION_IMPORT |
| config | object | Merged onto the stored config one top-level key at a time: each key named here replaces the stored value whole, and a key left out is kept. null clears the config. The merge is within one type — when this patch also changes `type`, the value here becomes the whole config instead, since the stored one was validated against a different framework's DTO. The resulting status is recomputed from validity. |
Responses
| Field | Type | Description |
|---|---|---|
| data | ImportDto | |
| id | string | Unique identifier for the import |
| name* | string | User-supplied display name for the import |
| description | string | Optional user-supplied description |
| fileLocation* | string | GCS path of the form "{bucket}/{tenantId}/{uuid}/{filename}" |
| originalFilename* | string | Filename as supplied by the user at upload time |
| mimeType* | string | Actual MIME type reported by GCS at create time |
| sizeBytes* | number | Actual size in bytes reported by GCS at create time |
| type* | string | What kind of import this is, which decides how its rows are used. COST_ANALYSIS_IMPORT is IMPACT, the should-cost/BOM pipeline, and the default for every caller that does not ask. SIGNAL_IMPORT is the deterministic paid-price-vs-band screen (DLN-503), available only to organizations holding the SIGNAL_ANALYSIS flag. TRANSACTION_IMPORT is a file of purchase lines (invoice or PO) mapped for the SIGNAL or IMPACT bulk route, available only to organizations holding the TRANSACTIONS flag. See src/imports/import-types.ts for the registry. enum: COST_ANALYSIS_IMPORT, SIGNAL_IMPORT, TRANSACTION_IMPORT |
| status* | string | Import lifecycle status. PENDING_CONFIG until the mapping is complete. A valid mapping moves a TRANSACTION_IMPORT to SUMMARY_READY, where its transactions are stored and nothing runs, until the uploader submits it; every other type goes straight to PENDING_APPROVAL. A transaction import on the SIGNAL route that an admin approves is PROCESSING ("Screening") until the last of its products is screened, then COMPLETED, PARTIALLY_COMPLETED or FAILED (ADR 0154); one on the IMPACT route is APPROVED and runs nothing yet. Every other approved type is PROCESSING. REJECTED is final. See ADR 0149. enum: PENDING_CONFIG, SUMMARY_READY, PENDING_APPROVAL, APPROVED, PROCESSING, COMPLETED, PARTIALLY_COMPLETED, FAILED, REJECTED |
| uploadedByUserId* | string | ID of the user that uploaded this import (from the request's auth context) |
| approvedByUserId | string | ID of the user who approved this import (set on approval): a Dalinea admin, or a client admin or owner. Unset for an AUTOMATIC dispatch. |
| approvedAt | string | Timestamp the import was approved |
| approvalMode | string | How the import was approved: INTERNAL (by Dalinea staff), EXTERNAL (by a client admin or owner, under the import-approvals flag for a type that runs the processor, or for any transaction import), or AUTOMATIC (dispatched by the system with no user action at all; never a transaction import). Unset while the import is still awaiting approval. The import-processor reads this to pick a row cap — anything that ran without Dalinea review is held to the tighter unattended limit. enum: INTERNAL, EXTERNAL, AUTOMATIC |
| rejectionReason | string | The reason the admin gave when rejecting the import, shown to the uploader. Optional; unset when the admin gave none or the import was not rejected. |
| config | object | Type-specific configuration. Validated by the service layer per type. |
| errorMessage | string | Error message if the import failed during processing |
| totalRowCount | number | Total rows the processor identified in the file |
| processedRowCount | number | Number of rows the processor has finished (successfully or otherwise) |
| productJobsSubmittedAt | string | When the import processor finished submitting an approved IMPACT bulk transaction import's runs, one per product (ADR 0162). Null until then, and cleared on each approval. The import does not end before it is set. |
| failedRowCount | number | Number of rows that failed processing |
| meta | object |
/imports/{id}
Delete an Import and its GCS object. Rejected while PROCESSING or busy.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required | |
| hard | query | string | required |
Responses
/imports/{id}/jobs
List child pipeline job rows for an import with their own status (paginated).
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required | |
| pageSize | query | number | optional | Default 50, max 200 |
| page | query | number | optional | Default 1 |
Responses
| Field | Type | Description |
|---|---|---|
| data* | ImportJobItemDto[] | |
| importJobId* | string | |
| pipelineJobId* | string | |
| rowIndex* | number | |
| rowData* | object | |
| submissionError | string | |
| pipelineStatus* | string | Live status from the Pipeline Service. |
| pipelineStep | string | |
| meta* | ImportJobsResponseMetaDto | |
| totalCount* | number | |
| aggregate* | ImportJobsAggregateDto | |
| total* | number | |
| completed* | number | |
| failed* | number | |
| inProgress* | number |
/imports/{id}/errors
List row-level errors for an import (paginated). Empty list when none.
Returns the failed job rows recorded during processing (rows with status "failed" and an error message). These cover both pre-submission and execution failures; live pipeline execution status is on GET /imports/:id/jobs.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required | |
| pageSize | query | number | optional | Default 50, max 200 |
| page | query | number | optional | Default 1 |
Responses
| Field | Type | Description |
|---|---|---|
| data* | ImportRowErrorDto[] | |
| rowIndex* | number | |
| rowData* | object | |
| errorMessage* | string | Why the row failed during processing. |
| meta* | ImportErrorsResponseMetaDto | |
| totalCount* | number |
/imports/{id}/download-url
Sign a download URL for the import file. Tenant-scoped.
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
| id | path | string<uuid> | required |
Responses
| Field | Type | Description |
|---|---|---|
| data | ImportDownloadUrlResponseDto | |
| downloadUrl* | string | |
| expiresAt* | string | |
| meta | object |