SporelineSporeline
Sign InStart free

Getting Started

  • Help Center
  • Getting Started

Operations

  • Dashboard
  • Scan
  • Blocks
  • Batches
  • Recipes

Printing

  • Label Templates

Lab

  • Cultures
  • Cryo Storage
  • Agar Plates
  • Liquid Cultures
  • Trials
  • Strains

Analytics

  • KPIs
  • Data Health
  • AI Insights

Logistics

  • Locations
  • Customers
  • Deliveries

Admin

  • Team
  • Settings
  • Billing
  • API Keys
  • Configuration
  • Enumerables
  • Integrations
  • Notifications
  • Import
  • Brand Profile
  • Products
  • Traceability
  • Compliance
  • Reports
  • Log Transcription
  • Switchboard

Guides

  • Your First Batch
  • Moving Blocks
  • Import from Spreadsheets
  • Lots and Spawn
  • Room Readings
  • The New Sporeline Look
  • Lots on Orders
  • Lot Transactions and Lot Audits
  • The Sterilization Log
  • Reports and Their Numbers
  • Auto-assign Lots and Pick Lists
  • Recipes and Planning a NOC Day
  • Stage Schedules and Due Today
  • Planning
  • Notification Rules
  • QC Checkpoints and Release
  • The Lab: Cultures, Cryo, Plates and Liquid Cultures
  • Traceability and Recall
  • Sporeline: Explore Your Lineage
  • Scanning on the Floor
  • Counts by Scan
  • Shared Handhelds
  • Zebra Handhelds
  • Orders and Packing Slips
  • QuickBooks Customers and Products
  • Accounting Export
  • Site Transfers
  • Raw Materials
  • Optional Features
  • Approve AI Changes
  • Configuration Tabs
  • Harvest Workflow
  • Contamination Alerts and the Weekly Digest
  • Label Printing
  • Team Setup
  • The Free Plan
  • Usage Recording
  • Signing Up and Farmhand on Free
  • Start Your Farm from an AI Assistant
  • Privacy and Your Data
  • Connect an AI Assistant
  • Ask Your Assistant
  • Appearance and Dark Mode
  • API Quickstart
  • Shopify Setup
  • Shopify Troubleshooting
  • Migrate With an AI Assistant
  • Spreadsheet Templates for Importing
  • Photos and Files

API

  • Reference
  • Authentication
  • Rate Limits
  • Error Codes

Endpoint Reference

Grouped listing of REST endpoints available via the Sporeline API.

developer

Endpoint Reference

All endpoints require a valid Authorization: ApiKey <api_key> header (see Authentication) and are tenant-scoped. Base URL: https://9uvbgolte1.execute-api.us-east-1.amazonaws.com/prod.

This reference groups endpoints by domain. For request/response schemas, examples, and auth setup, see the API Quickstart. To see live calls, sign in to the web app and watch the Network tab — every UI action uses the same REST API.

Production

Blocks

  • GET /blocks — list blocks (paginated, 15/page)
  • GET /blocks/{id} — get block detail
  • POST /blocks — create a block
  • PUT /blocks/{id} — update block metadata
  • POST /blocks/{id}/harvest — log a harvest event (singular path — not /harvests)
  • GET /blocks/{id}/environment — per-stage env retrospective (temp/RH/VPD/CO₂ stats, outlet on-time, cohort median). Returns env_available: false for stages with no AC Infinity room mapping. Data available for blocks whose locations have env_room_id set.
  • POST /blocks/{id}/label — generate/retrieve label URL

Harvests

  • GET /harvests — filterable list of terminal blocks (COMPLETED / CONTAMINATED / DISCARDED) for the Harvest Review page. Includes env_attribution_available flag for each block.

  • GET /harvests?view=lot-yields — lot harvests by species and strain (each group and lot row carries strain), one row per lot, filed under its most recent entry’s species and strain (blocks, wet and dry totals, average dry weight per block, dehydration ratio); optional from / to (YYYY-MM-DD)

    Query params: recipe_id, species, bag_type_id, from_date (ISO date), to_date, outcome (harvested | discarded | all, default all), limit (default 50, max 200), cursor (opaque pagination token).

Batches

  • GET /batches / GET /batches/{id}
  • POST /batches — create batch
  • PUT /batches/{id} — update batch
  • PATCH /batches/{id} — lot action on a lot-tracked batch (move, discard, sell, return, adjust, void, transition in the body’s action); send an Idempotency-Key header
    • A void the lot no longer qualifies for answers 409 with error.code LOT_NOT_PRISTINE and error.details.reason saying why: NOT_OPEN, ALLOCATED, HAS_ACTIVITY, TOO_OLD, or HAS_YIELD when the lot has harvests recorded. Delete its yield entries (delete_yield) first, then void it.
  • PATCH /batches/{id} with action record_yield (Worker+), edit_yield or delete_yield (Manager+) — a lot’s harvest: harvested_on (YYYY-MM-DD), wet_weight_g, optional dry_weight_g, block_count (defaults to the units on hand) and flush_number. The lot’s GET /batches/{id} lists them under yields with avg_dry_per_block_g and dehydration_ratio
  • DELETE /batches/{id} — archive a batch. A lot with units on hand, or with harvests recorded, is refused with 400 and a message saying which; delete a lot’s yield entries first.
  • POST /batches/plan-day — plan a day of lots (dry_run: true previews; a create needs an Idempotency-Key header)
  • POST /batches/{id}/bulk-transition — transition all blocks in a batch
  • POST /batches/{id}/labels — generate labels for every block in the batch

Recipes

  • GET /recipes / GET /recipes/{id}
  • POST /recipes — create
  • PUT /recipes/{id} — update

Events

  • POST /events — log a generic block/batch event (notes, state changes)

Locations

  • GET /locations / POST /locations / PUT /locations/{id} / DELETE /locations/{id}
  • GET /locations/{id}/environment?from=&to= — room-scoped env dashboard: per-metric time-series (temp/RH/VPD inside+outside, CO₂) with avg/min/max over the window, plus blocks/batches currently in the room. Window defaults to the trailing 7 days; series are bucket-downsampled to ≤500 points. Returns empty metrics when no readings landed for the location.

Bag Types

  • GET /bag-types / POST /bag-types / PUT /bag-types/{id} / DELETE /bag-types/{id}

Lab

Gated on the lab_tracking setting. Returns 403 if the tenant has not enabled lab features.

  • GET|POST /cultures, GET|PUT|DELETE /cultures/{id}
  • GET|POST /plates, GET|PUT|DELETE /plates/{id}, POST /plates/{id}/observations
  • GET|POST /liquid-cultures, GET|PUT|DELETE /liquid-cultures/{id}
  • GET|POST /trials, GET|PUT|DELETE /trials/{id}
  • GET|POST /strains, GET|PUT|DELETE /strains/{id}

Extraction

Available on every plan. An open extraction lot counts toward the plan’s active lab-item limit, like a culture or plate, so creating a lot or a transform child returns 402 UPGRADE_REQUIRED only when that limit is full.

An extraction lot tracks source material steeping in a menstruum — a maceration, tincture, decoction or blend. Two rules govern the whole resource:

  • On-hand is derived, never stored. It is the sum of the lot’s ledger deltas. There is no endpoint that sets a quantity; POST .../ledger is the only way a quantity ever moves.
  • Lots carry two identifiers. xlot_id is a ULID and is what every path takes. lot_code (for example LM-0902-M) is the human string on the jar; it is unique per tenant, generated server-side, and is never a path parameter.

Endpoints:

  • GET|POST /extraction-lots — list (filters: type, status, parent) and create
  • GET|PUT|DELETE /extraction-lots/{id} — fetch one (with derived on_hand), update metadata, soft-close
  • GET|POST /extraction-lots/{id}/ledger — paginated movements, and append one
  • POST /extraction-lots/{id}/transform — strain or blend: draw from one or more parents into a new child lot

Notes:

  • GET /extraction-lots omits on_hand by default, because deriving it means one ledger query per lot. Pass include=on_hand to get it on every row, or lot_code=LM-0902-M to look up one jar by its code.
  • PUT /extraction-lots/{id} updates name, notes, metered_abv, shelf_life_days, dates, status, ratio, menstruum (re-solved on save) and source_material (species, form, dry_g, material_kind). It never changes lot_code or lot_type.
  • Ledger deltas are signed. A draw larger than the lot’s on-hand is rejected with 409, as is a request that races a concurrent movement on the same lot.
  • RBAC follows the lab items: WORKER reads, MANAGER writes.

Scanning & Movement

  • POST /scan-progress — move a single resource to a new location/stage
  • POST /scan-progress/bulk — bulk move
  • GET /resources/{id}/movements — history for a single resource

Reports & Analytics

  • GET /reports/dashboard — dashboard summary (blocks by stage, tasks, recent activity)
  • GET /reports/dashboard?view=inventory — live and all-time inventory (Worker+; revenue is zeroed below Manager). Each by_product row has a measure: units (blocks or cultures) or weight_g for a HARVEST product, sold by weight, whose available, allocated and on_open_orders are grams of unsold harvest. harvest_truncated: true (with a harvest_hint) means the farm has too many unsold harvests to count, and HARVEST rows carry null stock rather than zero; lab_truncated does the same for culture products. A row without measure (an older API) counts units.
  • GET /reports/sessions/{session_id} — aggregated session report (batches grouped under a named session)
  • GET /kpi/summary?period=30d — financial KPI tiles
  • GET /data-health/score — tenant data health score
  • GET /contamination / GET /revenue-by-customer / GET /yield-by-batch / GET /yield-by-recipe
  • POST /analytics/snapshot — presigned URLs to the tenant’s Parquet export, for DuckDB-WASM (Manager+)
  • GET|POST /analytics/saved-queries, GET|DELETE /analytics/saved-queries/{id} (Manager+)

The /analytics endpoints require MANAGER or higher on every method. An API key with no role set authenticates as MANAGER, so key-based integrations are unaffected.

Traceability & Compliance

Available on every plan.

  • GET /lots/{id} — lot detail
  • POST /lots — register a lot
  • POST /lots/{id}/link — link parent lots to an existing lot
  • GET /lots/{id}/trace-forward — where this lot’s material ended up
  • GET /lots/{id}/trace-reverse — where this lot’s material came from
  • GET /lots/{id}/recall-scope — affected blocks, harvests, deliveries and customers (Manager+)
  • POST /lots/{id}/recall-export — start the complete recall walk for a lot, or join the one already running (Manager+)
  • GET /lots/{id}/recall-export — the newest recall walk for a lot (Manager+)
  • GET /lots/{id}/recall-export/{job_id} — one recall walk (Manager+)
  • GET /audit-report?block_id=... — enriched block audit data for branded reports

Traversal limits

The three walking endpoints above are capped so one lot graph cannot run unbounded, and each response says whether its cap was reached:

{
  "truncated": false,
  "traversal": {
    "truncated": false,
    "limit_hit": null,
    "entities_visited": 4002,
    "entity_limit": 10000,
    "depth_limit": 25
  }
}

limit_hit is entities, depth, time, edges, customers, missing_records, customer_records, batch_records, or null. missing_records means the walk followed a link to a lot id that has no record of its own (for example a parent id passed to POST /lots that was never registered), so that lot cannot be listed. recall-scope walks up to 10,000 linked lots and 25 levels, and names up to 2,000 distinct customers; trace-forward and trace-reverse stop at 500 lots and 10 levels, because they render a nested tree.

customers is capped separately because buyers hang off the affected deliveries rather than being nodes in the lot graph, so the entity cap does not bound them. time means the request’s shared 20s budget ran out — the walk and the customer lookups spend from the same clock, so the endpoint returns a labelled partial instead of exceeding the gateway timeout.

Treat truncated: true as “this list is incomplete.” The arrays beside it are a floor, not a total, and must not be used as the authoritative recall scope. When recall-scope stops at the lot, depth, page, time or customer limit, it also starts the complete recall walk below (or joins the one already running for that lot) and returns it as job, next to the same truncated and traversal fields it has always returned. Poll that job for the whole list.

The complete recall walk

A recall walk runs in the background and has no limit on how many lots, levels or buyers it follows. A long one carries on across several runs, and it never finishes short: it ends done with exact counts and its files, or failed with a reason. There is no truncated field on a job, on purpose.

POST /lots/{id}/recall-export                Manager+ (requireAccess(auth,"traceability","recall"))
     -> 202 { data: TraceJobView }           joins an in-flight job for the same lot (joined_existing: true)
GET  /lots/{id}/recall-export                Manager+ -> 200 { data: TraceJobView } newest job for the lot | 404 when none
GET  /lots/{id}/recall-export/{job_id}       Manager+ -> 200 { data: TraceJobView } | 404 (unknown, or another tenant's id)
GET  /lots/{id}/recall-scope                 unchanged, plus optional `job: TraceJobView` whenever truncated is true
                                             (the handler started or joined the job before answering)

TraceJobView {
  job_id: string; lot_id: string; kind: "recall_export";
  status: "queued" | "running" | "done" | "failed";
  requested_at: string; requested_by: string;         // user id, never an email
  started_at: string | null; finished_at: string | null;
  attempt: number; max_attempts: number;              // 3
  progress: { lots_read: number; level: number; counts_so_far: RecallExportCounts; updated_at: string } | null;
  counts: RecallExportCounts | null;                  // exact, set only when status is "done"
  error: string | null;                               // set only when "failed"
  download_url: string | null;                        // report ZIP, 15-minute signed URL minted per GET for Manager+
  customers_download_url: string | null;              // call sheet CSV, same rule
  download_expires_at: string | null;
  joined_existing: boolean;                           // POST response only; a poll always says false
}
RecallExportCounts { lots_walked, blocks, harvests, deliveries, pallets, orders, customers, lab_items, extraction_lots, other_lots }  // all numbers

What the counts mean:

  • lots_walked is every lot the walk read, the starting lot included.
  • orders is every order the walk reached; deliveries is the part of those whose product left the farm (shipped, delivered, invoiced or paid).
  • customers is every distinct buyer on those orders. Each one is on the call sheet, including a buyer whose customer record was deleted, who is named from the contact saved on their order.
  • lab_items counts cultures, agar plates and liquid cultures, extraction_lots counts macerations, tinctures and blends, and other_lots is everything else the walk read: batches, spawn, ingredients and any linked lot with no record of its own.

progress is null until the walk starts, then shows how far it has got. Between two runs of a long walk the status reads queued again while progress keeps the numbers it had reached. attempt counts the tries of the current run; a run that fails is tried again up to max_attempts times before the job stays failed.

The download links are signed fresh on every GET, last 15 minutes, and are only given for a done job. The ZIP holds the call sheet (affected_customers.csv), every affected order, pallet, block, harvest, lab item and extraction lot, the lot graph and a manifest.json with the same counts; the call sheet is also offered on its own.

Logistics

  • GET|POST /deliveries, GET|PUT /deliveries/{id}
    • PUT also accepts an INVOICED delivery (Manager+); the response’s outcome says whether its QuickBooks invoice took the change (updated, stale, not_in_quickbooks)
    • GET ?include=quickbooks — the QuickBooks connection as far as the delivery needs it; ?include=history — edits, voids, credit memos and refunds, newest first; ?include=qbo_invoice — the invoice’s lines as QuickBooks holds them (Manager+)
  • PATCH /deliveries/{id} with action:
    • qbo.push — send to QuickBooks now, or retry (Manager+)
    • qbo.void with reason — void the delivery and its QuickBooks invoice (Manager+)
    • qbo.credit_memo with reason and full: true or lines — issue a credit memo (Manager+, Idempotency-Key header required)
    • qbo.refund with reason and full: true or lines — refund a paid delivery (Manager+, Idempotency-Key header required)
  • GET|POST /customers, GET|PUT|DELETE /customers/{id}
  • POST /deliveries/{id}/invoice — generate invoice
  • POST /deliveries/{id}/packing-list — generate packing list

Labels & Printing

  • POST /blocks/{id}/label — single block label
  • POST /batches/{id}/labels — batch labels
  • GET|POST /label-templates, GET|PUT|DELETE /label-templates/{id}
  • POST /label-templates/{id}/render — render a template against context
  • POST /label-templates/{id}/preview — preview render without persisting

Marketplace

  • GET /marketplace — public marketplace listings (no auth required)
  • GET /suppliers/{id} — supplier profile
  • GET|POST /reservations, POST /reservations/{id}/cancel

Settings & Admin

  • GET|PUT /tenant/settings — tenant settings
  • GET|POST /tenant/team — team members
  • POST /tenant/invite — invite a user
  • GET|POST /api-keys, DELETE /api-keys/{id} — manage API keys (Manager or Owner, signed in; not available to API keys themselves)
  • GET|POST /cleaning-logs
  • GET|POST /enumerables

Import / Export

  • GET /export?entity=<type> — CSV export
  • POST /import — bulk import with transform pipeline
  • GET /config/export / POST /config/import — tenant configuration

Privacy (DSAR)

Available on every plan. Part of GDPR compliance workflow.

  • GET|POST /privacy/dsar — submit/read data subject access requests
  • POST /privacy/consent — record consent preferences
  • GET|POST /privacy/data-sharing — data sharing preferences

AI Insights

  • GET /ai/insights — list active insights
  • PATCH /ai/insights/{id} — dismiss an insight
  • POST /ai/label-design — generate a label layout from a prompt
  • GET /ai/label-design?job_id=... — poll async job status

Notes

  • Pagination: List endpoints return { data: [...], next_token?: "..." }. Pass next_token as a query param for the next page.
  • Response envelope: Every success is wrapped in { "data": ... }.
  • Errors: { "error": { "code": "...", "message": "...", "request_id": "...", "retry": true|false } }. Retry errors with retry: true using exponential backoff.
  • Idempotency: Send an Idempotency-Key header on a write (any unique string of up to 200 characters; a UUID is a good choice) and send the same key again on every retry of that write. The first request with a key claims it and runs. A retry with the same key and the same body gets the first answer back (with the header Idempotent-Replay: true) and does not run again. The same key with a different body is refused with 422 and code IDEMPOTENCY_KEY_REUSED. A retry that arrives while the first request is still running gets 409 with retry: true: wait a moment and send it again with the same key. A retry of a request that already completed but whose answer was too large to store gets 409 with code COMPLETED_ANSWER_NOT_KEPT (retry: false): the request did run, once, so read the record back instead of treating it as a refusal. A count’s submit and a transfer’s close never get that answer: their retries answer 200 with a shorter body (the record without its lines, and lines_omitted giving how many were left out). An error answer (4xx or 5xx) releases the key, so a corrected request may use it again, with one exception: POST /batches/plan-day creates a day in groups, and when it saves some lots and then stops (a 409 with code LOT_CODE_TAKEN or UPGRADE_REQUIRED, or a 500 with code INTERNAL_ERROR, each listing the saved lots in error.details.created), it keeps the key and stores that error answer. A retry with the same key and body gets the same error back (with Idempotent-Replay: true) and saves nothing more; if that answer was too large to store, the retry gets a 409 saying the request was already completed. To finish the day, resubmit what error.details.remaining_rows names (each request row’s row_index and the units still to make) with the session_id from error.details, under a new key. When no lot was saved, the key is released as usual. Keys belong to your farm only and expire after 7 days. Without a key, a write runs every time it is sent, so a blind retry of an order create can make a second order. Keys are honoured on: POST /deliveries (create an order), PATCH /deliveries/{id} (fulfillment actions such as allocate and allocate_batch), POST /deliveries/{id}/complaints, POST /batches, PATCH /batches/{id} (lot actions), POST /batches/plan-day (a key is required), POST /transfers, PATCH /transfers/{id} (close, update and cancel), POST /blocks/{id}/harvest, POST /blocks/{id}/transition, POST /blocks/bulk-transition, POST /scan-progress, POST /scan-progress/bulk, POST /agar-plates/batch-create, POST /materials and POST /materials/{id}/ledger. On other routes the header is ignored.
  • Idempotency on transfer ship and receive: PATCH /transfers/{id} with action ship or receive works differently from the rule above. A key is required; without one the request is refused with 400 and code IDEMPOTENCY_KEY_REQUIRED. The key is recorded with the scan itself, for that transfer only, and does not expire. A retry with the same key and the same scan (line_id, the location and quantity) records nothing again and answers 200 with the transfer as it is now (not as it was after the first scan), the scan’s leg, moved and ledger_entry, and "replayed": true in the body; there is no Idempotent-Replay header. Two copies sent at the same moment usually resolve the same way, with one recording the scan and the other replaying it. But if the first copy used up everything left on that line, the second can instead get 409 with code CONFLICT (retry: false) even though the scan was recorded. Before treating that 409 as a refusal, read the transfer back (GET /transfers/{id}) or resend once with the same key: a recorded scan answers with "replayed": true. The same key with a different scan on the same transfer is refused with 409 and code IDEMPOTENCY_KEY_REUSED. A scan that was refused recorded nothing, so its key may be used again.
  • Rate limits: See Rate Limits.

Need something not listed here? File a request at /admin/feedback from inside the app.

Try this on your own farm. Free for up to 100 blocks a month.

Start free

© 2026 Sporeline · Built by a mushroom farmer, for mushroom farmers.

iPhone app on the App StoreWhat's newSupportNeed help? Email chase@sporeline.com