{"schemaVersion":"1.0","canonicalUrl":"https://www.iamgeorgekelly.com/field-guide/contribution-api-acceptance-workflow","markdownUrl":"https://www.iamgeorgekelly.com/field-guide/contribution-api-acceptance-workflow/index.md","editorialNote":"AI-assisted research and drafting. Provider-specific claims link to primary sources. Frameworks are editorial proposals; worked examples are illustrative and are not employer performance results.","publication":{"name":"iamgeorgekelly","url":"https://www.iamgeorgekelly.com"},"citation":"George Kelly. A working contribution API integration: 14 acceptance cases iamgeorgekelly. Updated 2026-10-17. https://www.iamgeorgekelly.com/field-guide/contribution-api-acceptance-workflow","slug":"contribution-api-acceptance-workflow","title":"A working contribution API integration: 14 acceptance cases","description":"Run a real, bounded contribution API check. Inspect exact requests and responses, preserve losses and null ratios, and hold mismatched currency or cohort metadata before calling.","category":"Operations","status":"published","published":"2026-10-17","updated":"2026-10-17","author":"George Kelly","answer":"The recorded contribution API run met all 14 predeclared HTTP expectations: eight arithmetic scenarios returned HTTP 200 and six malformed requests returned HTTP 400. Five local caller checks also matched their expected outcomes, including four holds for missing or conflicting currency/cohort labels. Use the runnable workflow to check a client integration; the endpoint itself cannot establish whether the underlying business records belong together.","actions":[{"label":"Inspect the complete HTTP cases","section":"complete-http-panel"},{"label":"Download the runnable workflow","section":"run-the-workflow"},{"label":"Apply the currency and cohort gate","section":"caller-metadata-gate"}],"sections":[{"id":"recorded-acceptance","title":"What actually ran","paragraphs":["George Kelly’s internal acceptance run used Node.js 22.20.0 against https://www.iamgeorgekelly.com from 2026-10-04T14:22:52.767Z to 2026-10-04T14:22:54.266Z. The runner made 15 sequential GET requests: one for the OpenAPI description and one for each of 14 frozen synthetic cases. It made one attempt per request, with a 10-second timeout and no retries. The downloaded results preserve every request URL, timestamp, response status, selected headers, complete JSON body and acceptance finding.","The case inventory was authored before execution. It contains ordinary arithmetic, losses, zero denominators, decimal amounts, the inclusive input bound, and selected malformed requests. All 14 HTTP expectations passed. The five caller checks prepared one coherent request and held four incomplete or contradictory metadata records; these caller checks made zero HTTP requests.","This is a narrow integration result from an internal QA run. There were no language-model calls, independent users or business records in the panel. The custom User-Agent labels the requests as ContentQA so they can be separated from discovery and independent-use observations."],"download":{"url":"/downloads/contribution-api-acceptance-results.json","label":"Download the complete observed production run"},"sources":["gk-contract"]},{"id":"caller-metadata-gate","title":"Hold currency and cohort conflicts before the request","paragraphs":["The endpoint accepts exactly four monetary query parameters. Currency and cohort IDs are absent from its contract, so it cannot inspect whether revenue in USD was combined with costs in EUR, or whether September revenue was paired with August advertising spend. Adding currency=USD produces an unknown-parameter error; it does not enable a currency check.","The downloadable prepareScenario function takes a small caller record with amount, currency and cohortId for each monetary field. It requires four complete records, representable decimal amounts, one shared currency label and one shared cohort ID. It returns ready with a query string, or hold with reasons and a null request. The currency and cohort labels stay in the caller’s evidence record and are not sent as extra API parameters.","A matching label is a stated assumption. This gate does not authenticate a currency code, exchange rate, settled amount or cohort assignment. Resolve those facts in the source records before calculation. If conversion is needed, retain the conversion method and rate in that source record; this workflow performs no currency conversion."],"table":{"headers":["Caller case","Observed result","HTTP requests"],"rows":[["Four complete USD records, one cohort ID","ready; query prepared","0"],["USD revenue, EUR cost of goods","hold; currencies disagree","0"],["Variable costs lack a currency label","hold; currency unknown","0"],["Ad spend has a different cohort ID","hold; cohorts disagree","0"],["Revenue lacks a cohort ID","hold; cohort unknown","0"]]},"code":"{\n  \"netRevenue\": {\n    \"amount\": \"1000\",\n    \"currency\": \"USD\",\n    \"cohortId\": \"fictional-orders-2026-09-settled-v1\"\n  },\n  \"costOfGoods\": {\n    \"amount\": \"550\",\n    \"currency\": \"USD\",\n    \"cohortId\": \"fictional-orders-2026-09-settled-v1\"\n  },\n  \"variableCosts\": {\n    \"amount\": \"150\",\n    \"currency\": \"USD\",\n    \"cohortId\": \"fictional-orders-2026-09-settled-v1\"\n  },\n  \"adSpend\": {\n    \"amount\": \"200\",\n    \"currency\": \"USD\",\n    \"cohortId\": \"fictional-orders-2026-09-settled-v1\"\n  }\n}","sources":["gk-contract"]},{"id":"exact-request","title":"Build one exact request","paragraphs":["Use GET /api/tools/contribution with netRevenue, costOfGoods, variableCosts and adSpend, each present exactly once. Amounts must be non-negative decimal strings whose numeric values are finite and no larger than 1,000,000,000,000. The contract rejects signs, exponent notation, empty values, duplicates and unknown parameters. The six rejected cases below execute a selected subset of those boundaries.","The working client builds the query from a fixed four-field list with URLSearchParams. Node’s iterable constructor permits duplicate keys, so using URLSearchParams alone does not prove uniqueness. The caller wrapper determines the four keys; the endpoint independently rejects repeated values. Avoid appending an amount from a second source to a query that already contains that field.","The request is read-only arithmetic, with no external data retrieval or transactions. Amounts appear in the URL and may appear in client or hosting logs. The supplied panel uses fictional amounts; use the local browser calculator in the economics guide when a business figure should stay in the browser."],"code":"{\n  \"method\": \"GET\",\n  \"url\": \"https://www.iamgeorgekelly.com/api/tools/contribution?netRevenue=1000&costOfGoods=550&variableCosts=150&adSpend=200\",\n  \"headers\": {\n    \"Accept\": \"application/json\",\n    \"User-Agent\": \"iamgeorgekelly-ContentQA-contribution-api-acceptance/1.0\"\n  },\n  \"expectedStatus\": 200\n}","sources":["gk-contract","node-url"],"links":[{"slug":"roas-poas-profit","label":"Calculate a private scenario in the browser","reason":"Use the local calculator and examine the economic meaning of each result."}]},{"id":"response-contract","title":"Accept the result fields, not just HTTP 200","paragraphs":["For the fictional 1,000-revenue request, gross profit is 450; contribution before ads is 300; contribution after ads is 100. Revenue ROAS is 5×, gross-profit return is 2.25× and contribution return before ads is 1.5×. Break-even revenue ROAS is 10/3× and contribution margin before ads is 0.3, or 30%. These are different outputs with different numerators.","The checker requires formulaVersion 1.0, the returned four input values, all eight result fields, and the units and scope statements. It verifies the numeric expectations independently of the endpoint’s implementation. Integer-cents arithmetic and rational ratios also reconcile the eight successful fixture expectations in the repository tests.","Monetary output is unrounded JavaScript arithmetic. Ratios are multiples, and margin is a decimal fraction. The checker uses a tolerance of eight times Number.EPSILON scaled to the expected result; it requires exact null values for undefined results. Presentation rounding belongs after acceptance, while the raw result remains available for review."],"code":"{\n  \"formulaVersion\": \"1.0\",\n  \"input\": {\n    \"netRevenue\": 1000,\n    \"costOfGoods\": 550,\n    \"variableCosts\": 150,\n    \"adSpend\": 200\n  },\n  \"grossProfit\": 450,\n  \"contributionBeforeAds\": 300,\n  \"contributionAfterAds\": 100,\n  \"revenueRoas\": 5,\n  \"grossProfitReturn\": 2.25,\n  \"contributionReturn\": 1.5,\n  \"breakEvenRoas\": 3.3333333333333335,\n  \"contributionMarginBeforeAds\": 0.3,\n  \"units\": \"Use the same currency for all monetary inputs and outputs. Returns are multiples; contributionMarginBeforeAds is a decimal fraction.\",\n  \"scope\": \"Arithmetic scenario only; excludes fixed overhead and does not establish incrementality.\"\n}","sources":["gk-contract"]},{"id":"null-and-loss-boundaries","title":"Preserve zero, null and a loss as different outcomes","paragraphs":["With zero ad spend, revenueRoas, grossProfitReturn and contributionReturn are null because their denominator is zero. Contribution after ads is still 300 in the worked example. Converting these null ratios to zero would turn an undefined return into an observed zero return.","With zero revenue and positive spend, revenueRoas is 0, while contributionMarginBeforeAds and breakEvenRoas are null. In the zero-revenue fixture, costs of 55 and 15 plus ad spend of 20 leave contribution after ads of −90. Preserve both the zero revenue return and the monetary loss.","For revenue 1,000, cost of goods 700, other variable costs 400 and ad spend 200, contribution before ads is −100 and after ads is −300. Revenue ROAS remains 5×, contribution return is −0.5×, and breakEvenRoas is null. The endpoint also returns null break-even ROAS when contribution before ads is exactly zero. A client must not clip a loss or present a break-even target for a non-positive pre-ad contribution."],"sources":["gk-contract"]},{"id":"complete-http-panel","title":"The complete 14-case HTTP panel","paragraphs":["Every row below is retained in the downloadable cases and observed run. Expected numeric values are frozen as decimal or rational strings, including exact null expectations. The request URL and full response for every row are preserved in the results artifact; the table selects the field that makes the boundary easiest to inspect.","The 1e12 limit applies to each input. The upper-bound case accepts four inputs of 1,000,000,000,000 and returns contribution after ads of −2,000,000,000,000. That output is finite and the input bound is inclusive."],"table":{"headers":["Case ID","Observed HTTP","Boundary preserved"],"rows":[["positive-control","200","Contribution after ads 100; margin 0.3"],["negative-contribution","200","Contribution after ads −300; contribution return −0.5"],["zero-ad-spend","200","Three spend ratios null; contribution after ads 300"],["zero-revenue","200","Revenue ROAS 0; margin and break-even ROAS null"],["zero-pre-ad-margin","200","Contribution return 0; break-even ROAS null"],["all-zero","200","Monetary results 0; all five ratios/margin null"],["decimal-inputs","200","Contribution after ads 10; revenue ROAS 5.025"],["upper-input-bound","200","1e12 inputs accepted; contribution after ads −2e12"],["missing-ad-spend","400","Missing value rejected; no zero default"],["duplicate-ad-spend","400","Repeated adSpend rejected even when both equal 200"],["negative-input","400","adSpend=−1 rejected"],["exponent-input","400","adSpend=2e2 rejected"],["above-input-bound","400","adSpend=1000000000001 rejected"],["unknown-currency-parameter","400","currency=USD rejected as unknown"]]},"download":{"url":"/downloads/contribution-api-acceptance-cases.json","label":"Download all requests, caller metadata and frozen expectations"}},{"id":"error-handling","title":"Keep transport errors separate from rejected input","paragraphs":["An expected 400 response is a passed negative case when the exact error body matches its frozen expectation. It is not a successful calculation. For example, the duplicate case returns an error stating that adSpend is required exactly once as a non-negative decimal number.","A timeout, redirect, invalid JSON, unexpected status or response-field mismatch is an acceptance failure. The runner records the error and continues to the remaining frozen cases without retrying. It exits with a failure status if any contract, HTTP or caller expectation fails. A successful page fetch cannot substitute for an API result.","The checker also inspects the calculation response headers: application/json, Cache-Control: no-store, Access-Control-Allow-Origin: *, X-Robots-Tag: noindex, and the service-description link to /openapi.json. The OpenAPI description itself must return HTTP 200 JSON with the expected operation, four required query parameters, limits, formula version and nullable response fields."],"sources":["gk-contract"]},{"id":"run-the-workflow","title":"Run the portable workflow","paragraphs":["Download the runner, cases, source snapshot and recorded results into one folder with their original filenames. Node.js 22 or later is sufficient; the runner uses built-in fetch, URLSearchParams, AbortSignal.timeout and filesystem/hash modules. The retained public-endpoint run used Node.js 22.20.0 with no third-party packages.","The default command and --check verify the recorded responses, caller decisions, inventory totals and source hashes offline. They make zero network requests. This reproduces acceptance of the retained evidence; it does not remeasure today’s service.","Use --run to execute the frozen synthetic panel again. It fetches the contract and makes the 14 sequential API requests, while keeping the five caller checks local. A local or preview origin can be supplied with --base. HTTPS is required for a remote origin; HTTP is allowed on localhost. Save a new output filename to preserve the original recorded run."],"code":"{\n  \"offline\": \"node contribution-api-acceptance.mjs --check\",\n  \"publicEndpoint\": \"node contribution-api-acceptance.mjs --run > contribution-api-acceptance-rerun.json\",\n  \"localEndpoint\": \"node contribution-api-acceptance.mjs --run --base http://localhost:3000 > contribution-api-acceptance-local.json\"\n}","download":{"url":"/downloads/contribution-api-acceptance.mjs","label":"Download the dependency-free Node.js acceptance runner"},"sources":["node-runtime"]},{"id":"working-integration","title":"Use the caller wrapper in an integration","paragraphs":["The README includes a complete Node.js client example. It imports prepareScenario and verifyResponse, loads the fictional metadata record from the cases file, holds any metadata conflict, sends the four prepared amounts, collects the JSON response and selected headers, and accepts the result against the positive-control expectation.","An implementation should keep the caller’s currency, cohort ID and source record beside the returned JSON because those fields are absent from the API response. Return the arithmetic scope with the result so contribution after ads is interpreted before fixed overhead. A calculation alone cannot authorize a budget or establish advertising incrementality.","Use the existing contribution economics and returns guides to establish the record definitions before supplying real amounts. A client adapter should retain the endpoint’s null values and negative results through any display, export or downstream explanation."],"download":{"url":"/downloads/contribution-api-acceptance-readme.md","label":"Download setup instructions and the complete client example"},"links":[{"slug":"returns-adjusted-contribution","label":"Reconcile a settled cohort before calculating","reason":"Keep refunds, recovered inventory and order costs on a consistent basis."},{"slug":"roas-poas-profit","label":"Choose the return measure for the decision","reason":"Separate revenue return, contribution return and contribution after ads."}]},{"id":"provenance-and-scope","title":"Know what the hashes and pass count establish","paragraphs":["The source snapshot contains the exact reviewed local formula, HTTP route and OpenAPI route, each with a SHA-256 digest and the local Git revision. The recorded run additionally hashes the runner, frozen cases, source snapshot and returned contract. These identifiers let a reviewer detect drift in the supplied evidence bundle.","Local source hashes do not independently prove which code was deployed behind the public endpoint. The complete observed requests and responses establish the externally observed result at the recorded time. The selected cases are an authored acceptance panel rather than a random or exhaustive sample.","The 14/14 HTTP result describes this version, endpoint, runtime and case inventory. It measures no language-model performance, independent adoption, repeat use, load tolerance, uptime, traffic effect or business gain. Currency/cohort checks establish label consistency inside the caller; actual accounting and data provenance remain outside this panel."],"download":{"url":"/downloads/contribution-api-acceptance-source-snapshot.json","label":"Download reviewed source contents and hashes"},"sources":["openapi-role"]},{"id":"next-integration-decision","title":"Recheck the contract when the client or formula changes","paragraphs":["Treat a changed formula version, required field, error response or null rule as a review trigger. Keep the previous run, revise expectations only after inspecting the change, and retain a new complete run under a new version or filename. Updating the expected result merely to make a failing check pass removes the value of the acceptance panel.","For a new client, add the client-specific boundary without replacing these controls: for example, whether an export preserves null ratios or whether a display keeps contribution negative. Record those additional checks as a separate extension so the original 14-case denominator remains visible."],"list":["Confirm the operation and formula version before trusting a saved adapter.","Hold absent or contradictory currency/cohort metadata.","Require all four inputs exactly once and reconcile returned inputs.","Preserve negative amounts in outputs and exact null semantics.","Keep every case, including expected rejections and unexpected failures.","Retain the caller evidence, raw response, version and time together."],"links":[{"slug":"supervised-commerce-workflows","label":"Put the calculation inside a reviewable operating decision","reason":"Carry source evidence and authorization through a proposed commercial change."}]}],"sources":[{"id":"gk-contract","title":"George Kelly: contribution calculation OpenAPI contract","url":"https://www.iamgeorgekelly.com/openapi.json","checked":"2026-10-04","note":"First-party contract captured as HTTP 200 JSON during the actual run. Supports required amounts, scope, formula version, bounds, error policy and nullable outputs. Full observed contract and responses are preserved in the downloadable run; matching caller metadata is an original wrapper rule."},{"id":"node-runtime","title":"Node.js v22.20.0: globals, fetch and AbortSignal.timeout","url":"https://raw.githubusercontent.com/nodejs/node/v22.20.0/doc/api/globals.md","checked":"2026-10-04","note":"Official versioned Node.js source documentation supports built-in fetch and the timeout signal. The supplied workflow was actually executed under Node.js v22.20.0; this source does not establish compatibility with every client or runtime."},{"id":"node-url","title":"Node.js v22.20.0: URLSearchParams","url":"https://raw.githubusercontent.com/nodejs/node/v22.20.0/doc/api/url.md","checked":"2026-10-04","note":"Official versioned documentation supports iterable query construction, serialization and the fact that iterable input permits duplicate keys. The fixed-field caller gate and duplicate rejection are evaluated in this workflow."},{"id":"openapi-role","title":"OpenAPI Specification 3.1.1","url":"https://spec.openapis.org/oas/v3.1.1.html","checked":"2026-10-04","note":"Supports the role of an OpenAPI description as an interface and semantics description for HTTP APIs. The contract checks are a selected workflow subset, not an OpenAPI certification or provider-admission test."}],"related":["roas-poas-profit","returns-adjusted-contribution","supervised-commerce-workflows","agentic-commerce"]}