Deconfound API

From an idea
to a real sample.

Formulate a flavor, quote a trial, and retrieve the lab’s observations. One API for your application or agent.

Base URLhttps://labs.deconfound.com/api/v2
Payment mode available from /lab-status

Read GET /lab-status for the current intake and payment mode.

Start here

Connect once. Build your flow.

Server-side requests

Send your key in X-Deconfound-Api-Key. Use the setup below, then run the numbered examples in order. JavaScript examples share the api() helper.

export API_BASE="https://labs.deconfound.com/api/v2"
export DECONFOUND_API_KEY="YOUR_API_KEY"

# Create once. Save these values for retries of each operation.
export CREATE_KEY="$(uuidgen)"
export GENERATE_KEY="$(uuidgen)"
export ORDER_KEY="$(uuidgen)"
export PAYMENT_KEY="$(uuidgen)"

Authentication An Authorization / Bearer header is not accepted. ChatGPT and Sites tokens do not authenticate this API.

Retry keys Persist each UUID and its request body before sending. Reuse both for a retry; generate a new key only for a new operation.

The core workflowAll responses below are illustrative excerpts.
01

Find your ingredients

GET/ingredients

Search the flavor library for ingredient IDs and formulation profiles. Use the returned catalog version when submitting an exact recipe.

Request
curl --fail-with-body "$API_BASE/ingredients?q=lime&limit=5" \
  -H "X-Deconfound-Api-Key: $DECONFOUND_API_KEY"
Illustrative response · abbreviated
JSON
{
  "catalogVersion": "<current catalog version>",
  "ingredients": [
    {
      "id": "flavor.lime",
      "name": "Lime"
    }
  ]
}

The customer catalog does not expose supplier names, purchasing details or internal costs.

02

Describe an idea

POST/ideas

Save a brief for a beverage or water-based flavor. This creates an idea; generation is a separate request.

Request
curl --fail-with-body "$API_BASE/ideas" \
  -H "X-Deconfound-Api-Key: $DECONFOUND_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $CREATE_KEY" \
  --data '{
    "prompt": "A crisp still lime drink with a dry finish.",
    "kind": "beverage",
    "batchMassG": 500,
    "includeJuices": false
  }'
Illustrative response · abbreviated
JSON
{
  "idea": {
    "id": "<idea ID>",
    "status": "created",
    "revision": 1,
    "recipes": []
  }
}

Save idea.id as IDEA_ID and idea.revision as IDEA_REVISION. Prompts accept 3–1,500 characters; the standard orderable batch is 100–500 g.

03

Generate a recipe

POST/ideas/{id}/generate

Two AI passes develop the approach and formulate against the library. The request can run for up to 240 seconds; allow a 300-second client timeout.

Request
curl --fail-with-body --max-time 300 \
  "$API_BASE/ideas/$IDEA_ID/generate" \
  -H "X-Deconfound-Api-Key: $DECONFOUND_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $GENERATE_KEY" \
  --data "$(printf '{"revision":%s}' "$IDEA_REVISION")"
Illustrative response · abbreviated
JSON
{
  "idea": {
    "id": "<idea ID>",
    "status": "succeeded",
    "revision": 3,
    "recipes": [
      {
        "name": "Crisp Still Lime",
        "batchMassG": 500,
        "status": "untested",
        "requiresAcceptance": false,
        "variantId": null,
        "ingredients": [
          {
            "ingredientId": "flavor.lime",
            "sourceMassG": 0.5
          },
          {
            "ingredientId": "acid.citric",
            "sourceMassG": 0.6
          },
          {
            "ingredientId": "sweet.sucrose",
            "sourceMassG": 30
          },
          {
            "ingredientId": "base.water",
            "sourceMassG": 468.9
          }
        ],
        "services": {
          "quote": {
            "currency": "USD",
            "totalCents": 6900
          }
        }
      }
    ],
    "reason": null,
    "gaps": []
  }
}

Use the returned revision for quoting. A succeeded idea can still have an empty recipes array: inspect reason and gaps. Before quoting an alternative, obtain the customer’s acceptance and set recipe.acceptAlternative to true.

createdrunningsucceeded or failed

Generation develops an untested recipe. Only recorded kitchen work produces test results.

04

Get the customer’s price

POST/order-quotes

Recipe trials have recipe-specific prices. Quote the exact recipe and deliverables: ingredients, quantities, preparation and selected versions determine the price. The scope hash binds the order to this quote.

Request
# Use the latest revision returned by generation.
SELECTION=$(cat <<JSON
{
  "recipe": {
    "source": "idea",
    "ideaId": "$IDEA_ID",
    "ideaRevision": $IDEA_REVISION,
    "replicates": 1
  },
  "deliverables": ["photo", "taste"],
  "variationIds": []
}
JSON
)
curl --fail-with-body "$API_BASE/order-quotes" \
  -H "X-Deconfound-Api-Key: $DECONFOUND_API_KEY" \
  -H "Content-Type: application/json" \
  --data "$SELECTION"
Illustrative response · abbreviated
JSON
{
  "quote": {
    "kind": "service_quote",
    "status": "provisional",
    "currency": "USD",
    "totalCents": 6900,
    "scopeHash": "<64-character quote hash>",
    "versions": 1,
    "preparations": 1,
    "batchMassG": 500,
    "deliverables": [
      "photo",
      "taste"
    ],
    "binding": false
  }
}

Save the selection unchanged and quote.scopeHash as QUOTE_HASH. Show the price and scope to your customer before submitting an order. Always use the returned price. Only a complete supported quote can be ordered. A null total needs a supported alternative or corrected selection, never a review order.

Have an exact recipe?

Skip idea generation. Replace selection.recipe with this object, using the current catalog version. Quantities are grams of the source ingredient; these sum to 500 g, including 468.9 g water. Custom preparation is also supported; see the exact-recipe schema in OpenAPI.

Exact recipe · example
{
  "source": "ingredients",
  "name": "Still lime drink",
  "catalogVersion": "<current catalog version>",
  "batchMassG": 500,
  "carbonated": false,
  "ingredients": [
    {
      "ingredientId": "flavor.lime",
      "sourceMassG": 0.5
    },
    {
      "ingredientId": "acid.citric",
      "sourceMassG": 0.6
    },
    {
      "ingredientId": "sweet.sucrose",
      "sourceMassG": 30
    },
    {
      "ingredientId": "base.water",
      "sourceMassG": 468.9
    }
  ]
}
Add research sample shipping

Keep the same recipe and merge these fields into your quote selection. The API estimates overnight cool-packed shipping for eligible contiguous-US destinations. It is a planning estimate, not a carrier booking. Shipped samples are for research only and are not for human or animal consumption. Include the recipient, street address and city in shippingAddress when ordering.

Shipping selection · fields to merge
{
  "deliverables": [
    "photo",
    "taste",
    "ship"
  ],
  "variationIds": [],
  "destination": {
    "country": "US",
    "region": "CA",
    "postalCode": "94133"
  }
}
Compare recipe versions

Inspect trialProposal for available variations before selecting reduced_20 and reduced_40 in variationIds. The original and selected versions are prepared separately, with a record for each. The quoted price and preparation count update with your selection.

05

Start checkout

POST/orders

Show the exact recipe, quoted total and trial terms from /order-api.md. After the customer agrees, submit the same selection and quote hash with checkout.acceptQuote:true. Keep the returned order ID.

Request
# Run only after customer approval. Set CONTACT_EMAIL first.
curl --fail-with-body "$API_BASE/orders" \
  -H "X-Deconfound-Api-Key: $DECONFOUND_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $ORDER_KEY" \
  --data "$(cat <<JSON
{
  "selection": $SELECTION,
  "quoteHash": "$QUOTE_HASH",
  "shareWithTeam": true,
  "contactEmail": "$CONTACT_EMAIL",
  "checkout": {"acceptQuote": true}
}
JSON
)"
Illustrative response · abbreviated
JSON
{
  "order": {
    "id": "<order ID>",
    "kind": "order",
    "orderType": "service_order",
    "status": "new",
    "orderStatus": "awaiting_checkout",
    "revision": 1,
    "payment": {
      "id": "<payment ID>",
      "provider": "stripe",
      "mode": "test",
      "status": "pending",
      "amountCents": 6900,
      "currency": "USD",
      "fundsCaptured": false
    }
  }
}

contactEmail is required for API-key requests. This saves the fixed quote and starts checkout without a lab-confirmation step. It does not start kitchen work, schedule shipping or capture a payment.

The example uses Stripe sandbox. If /lab-status instead returns test, use the internal simulator: POST /orders/{id}/test-payment records a checkout outcome with {"outcome":"succeeded"} and its own idempotency key. No funds are captured, and success puts the order in Ordered - Pending Lab.

Test payment request body
{"outcome":"succeeded"}
06

Open secure checkout

POST/orders/{id}/stripe-checkout

When /lab-status reports stripe_test, create a hosted Stripe sandbox checkout for the accepted order. Send the returned URL to the customer to complete checkout. No real money is charged.

Request
curl --fail-with-body "$API_BASE/orders/$ORDER_ID/stripe-checkout" \
  -H "X-Deconfound-Api-Key: $DECONFOUND_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $PAYMENT_KEY" \
  --data "$(printf '{"paymentId":"%s"}' "$PAYMENT_ID")"
Illustrative response · abbreviated
JSON
{
  "orderId": "<order ID>",
  "revision": 2,
  "orderStatus": "awaiting_checkout",
  "payment": {
    "id": "<payment ID>",
    "provider": "stripe",
    "mode": "test",
    "status": "pending",
    "amountCents": 6900,
    "currency": "USD",
    "fundsCaptured": false
  },
  "checkout": {
    "url": "https://checkout.stripe.com/c/pay/cs_test_example",
    "sessionId": "cs_test_example",
    "expiresAt": "<expiry timestamp>"
  }
}

Save order.payment.id as PAYMENT_ID. The server uses the saved total; never send your own amount. After checkout, read GET /orders/{id} until the verified payment is reflected. Display ordered_pending_lab as Ordered - Pending Lab. Returning from Stripe does not prove payment. Keep the same body/key after a lost response.

07

Retrieve the lab report

GET/orders/{id}/results

Read the recorded batches, ingredient amounts, preparation, photos and observations as the lab adds them. Results begin empty and include reasons for omitted assessments.

Request
# Save the returned order ID as ORDER_ID.
curl --fail-with-body "$API_BASE/orders/$ORDER_ID/results" \
  -H "X-Deconfound-Api-Key: $DECONFOUND_API_KEY"
Illustrative response · abbreviated
JSON
{
  "orderId": "<order ID>",
  "fulfillmentStatus": "pending",
  "output": null,
  "results": {
    "status": "not_started",
    "expectedBatches": 1,
    "recordedBatches": 0,
    "batches": [
      {
        "recipeId": "original",
        "label": "Original",
        "replicate": 1,
        "targetMassG": 500,
        "record": null
      }
    ],
    "history": []
  }
}

Read output first for the published sample descriptions, photos and analysis; output.completedAt marks completion. Advanced results can still be not_started. Reported means records were submitted, not that every test passed.

Production behavior

Handle the next step.

Errors include a code, message, retryability flag and request ID. Preserve that ID when reporting a problem; use the response to decide whether to retry or ask the customer to act.

401 / 403

Check authentication and scope

Use an active key with the required permissions. Keys can only access their owner’s resources.

409

Refresh state or resolve a conflict

QUOTE_CHANGED: obtain and show a fresh quote. IDEMPOTENCY_CONFLICT: the key was reused with a different body. LAB_PAUSED: wait for intake to reopen.

422

Correct the request

Check validation details, supported ingredients and methods. Set acceptAlternative: true only after the customer accepts the proposed compromise, before quoting or ordering it.

429 / 5xx

Retry deliberately

Honor Retry-After and error.retryable. If the result of a write is uncertain, retry the unchanged body with its original key; do not create a second order.

A generation request timed out?

Read GET /ideas/{id} first. While it is running, wait and check again. Retrying the same generation with the same key and body can return 202. After a confirmed failure, read the latest revision before starting a new attempt with a new key.

Your account

API keys

90-day expiration

Give your application or agent its own key. Keep it on the server; never ship it in a public browser bundle.

Agents & MCP

Bring your agent.

Agents can use the same REST endpoints, scoped API keys and explicit quote approval flow.

Native MCPConnect with your API key

Use /mcp with the X-Deconfound-Api-Key header. Copy connection and tool examples.