> ## Documentation Index
> Fetch the complete documentation index at: https://docs.spikeapi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Lab Reports API Implementation Guide

> Complete code examples and integration patterns for the Lab Reports API

## About

This guide provides complete implementation details and best practices for integrating the Lab Reports API into your applications. For detailed API specifications and data types, refer to the [API Reference](/api-reference/lab-reports-upload-lab-report).

<Note>
  **Authentication**: The code examples in this guide focus on the lab report analysis functionality. For authentication implementation details, see the [Authentication documentation](/api-docs/authentication). You'll need to add Bearer token into the Authorization header to all API requests.
</Note>

## API Endpoints

The Lab Reports API provides three main endpoints for managing lab reports:

* **[`POST /lab_reports`](/api-reference/lab-reports-upload-lab-report)** — upload lab report documents for AI-powered analysis
* **[`GET /lab_reports`](/api-reference/lab-reports-list-lab-reports)** — retrieve a list of lab reports by the time range
* **[`GET /lab_reports/{lab_report_id}`](/api-reference/lab-reports-get-lab-report)** — retrieve a specific lab report by ID

## Document Preparation

For optimal document capture guidelines, see the [Document Guidelines](/lab-reports/overview#document-guidelines) in the API overview.

## POST Request Body

### Processing Modes

The processing mode is controlled by the `wait_on_process` parameter. The API supports two processing modes, each suited for a different use case:

#### Asynchronous Processing

Returns immediately and processes the document in the background. Ideal for user-facing applications where immediate feedback is important. See the [Asynchronous Processing](/lab-reports/async) guide for complete implementation details, webhook configuration, and code examples.

#### Synchronous Processing

```mermaid theme={null}
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#ffffff", "primaryTextColor": "#161a1d", "primaryBorderColor": "#ded1e4", "lineColor": "#006ad4", "secondaryColor": "#f2f6f9", "tertiaryColor": "#eef1f4", "background": "#ffffff", "mainBkg": "#ffffff", "secondBkg": "#f2f6f9", "actorBkg": "#eef1f4", "actorTextColor": "#161a1d", "actorLineColor": "#006ad4", "signalColor": "#006ad4", "signalTextColor": "#161a1d", "labelBoxBkgColor": "#ffffff", "labelTextColor": "#4f5356", "loopTextColor": "#4f5356", "noteBkgColor": "#f2f6f9", "noteTextColor": "#4f5356", "activationBkgColor": "#ded1e4", "activationBorderColor": "#9ea1a5"}}}%%
sequenceDiagram
    participant A as App
    participant S as Spike API

    A->>+S: POST /auth/hmac<br/>HMAC signature
    S-->>-A: Access Token

    A->>+S: POST /lab_reports<br/>Base64 file + token (wait_on_process: true)
    activate S
    S-->>A: 200 OK<br/>Complete lab report with status: "completed"
    deactivate S

    S->>-A: POST webhook URL<br/>Complete lab report
    activate A
    A-->>+S: 200 OK
    deactivate A
```

Waits for complete analysis before responding. Best for batch processing, server-to-server integrations, or when you can handle longer response times:

**Request:**

```json theme={null}
{
  "mime_type": "application/pdf",
  "body": "base64-encoded-document-data",
  "filename": "lab_report_2024_01_15.pdf",
  "wait_on_process": true
}
```

**Response:**

```json theme={null}
{
  "lab_report": {
    "record_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
    "status": "completed",
    "collection_date": "2024-01-15",
    "result_date": "2024-01-16",
    "sections": [
      {
        "loinc_code": "58410-2",
        "loinc_common_name": "CBC panel - Blood by Automated count",
        "original_section_name": "HEMATOLOGY",
        "results": [
          {
            "loinc_code": "6690-2",
            "loinc_common_name": "Leukocytes [#/volume] in Blood",
            "original_test_name": "White Blood Count",
            "original_unit_name": "K/uL",
            "standard_unit_name": "K/uL",
            "value": 7.2,
            "normal_min": 4.0,
            "normal_max": 11.0,
            "within_normal_range": true,
            "require_human_review": false
          }
        ]
      }
    ],
    "uploaded_at": "2024-01-15T10:30:04.521Z",
    "modified_at": "2024-01-15T10:30:12.132Z"
  }
}
```

## Response Body

### Processing Status

Analysis of the lab report progresses through these states:

* **pending** — analysis has been queued
* **processing** — an AI model is actively analyzing the document
* **completed** — analysis finished successfully with results
* **failed** — processing failed due to unreadable content or technical issues

REST API responses and webhook notifications will always include the `status` field.

```json theme={null}
{
  "record_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
  "status": "processing",
  "uploaded_at": "2024-01-15T10:30:04.521Z"
}
```

Webhook notifications can have `status` field with values `completed` or `failed` only. Always handle different response statuses in your application logic.

### Error Scenarios

#### Processing Failures

These occur when the API successfully receives your request but the AI analysis fails. The response will have HTTP 200 status with `"status": "failed"`.

When `status` is `failed`, check the `parsing_error` field for specific details:

* **Document not a lab report** — a document contains nonmedical content
* **Poor document quality** — blurry, dark, or low-resolution images/scans
* **Unreadable text** — OCR could not extract reliable text
* **AI processing timeouts** — although AI providers typically have fast [Response Times](/lab-reports/overview#response-times), processing may occasionally take longer than expected due to reasons that cannot be foreseen.
  To manage this, the upload process will time out if synchronous processing takes longer than 3 minutes and asynchronous processing takes longer than 5 minutes.

#### Request Errors

These occur when there's an issue with the request itself, returning non-200 HTTP status codes before analysis begins.

* **Decoding File Error** — if the file is corrupted or improperly encoded, and therefore cannot be decoded, then HTTP 400 will be returned.
  ```json theme={null}
  {
    "title": "Bad Request",
    "status": 400,
    "detail": "invalid base64 body"
  }
  ```

* **Invalid File Format** — Lab Reports supports JPEG, PNG, GIF, WebP, and PDF formats. When a file format is not supported, HTTP status code 400 will be returned.

```json theme={null}
{
    "title": "Bad Request",
    "status": 400,
    "detail": "unsupported mime type: application/vnd.openxmlformats-officedocument.wordprocessingml.document"
}
```

For HTTP status code errors (400, 401, 422, etc.) and general API error handling, see the [Error Handling documentation](/api-docs/errors).

## Retrieving Results

### Getting Results

For asynchronous processing, use webhooks for real-time notifications. See the [Asynchronous Processing Guide](/lab-reports/async) for complete webhook implementation details.

If webhooks are not yet available, you can check status using the [`GET /lab_reports/{lab_report_id}`](/api-reference/lab-reports-get-lab-report) endpoint.

Retrospectively, a list of all uploaded lab reports can be retrieved using the [`GET /lab_reports`](/api-reference/lab-reports-list-lab-reports) endpoint by providing the time range.

## Best Practices

### 1. Choose the Right Processing Mode

* **Asynchronous** — for user-facing applications, mobile apps, and when you want immediate feedback
* **Synchronous** — for batch processing, server-to-server integrations, and when you can handle longer response times

### 2. Implement Proper Error Handling

* Handle network errors gracefully
* Provide meaningful error messages to users
* Implement retry logic for transient failures

### 3. Optimize Document Upload

* Use high-resolution scans (300+ DPI) for better OCR results
* Validate document size and format before upload
* Show upload progress for large documents

## Code Examples

Here are complete implementation examples in multiple programming languages:

<CodeGroup>
  ```python [Python] theme={null}
  import os
  import requests
  import base64
  import hmac
  import hashlib
  import json

  SPIKE_API_BASE_URL = os.getenv("SPIKE_API_BASE_URL")
  SPIKE_APP_HMAC_KEY = os.getenv("SPIKE_APP_HMAC_KEY")

  try:
      SPIKE_APP_ID = int(os.getenv("SPIKE_APP_ID"))
  except (ValueError, TypeError):
      raise ValueError("SPIKE_APP_ID must be an integer.")

  # Implementation depends on your authentication method
  # See: /api-docs/authentication for complete details
  class SpikeAuth:
      def __init__(self, end_user_id):
          self.user_id = end_user_id
          self.app_id = SPIKE_APP_ID
          self.hmac_key = SPIKE_APP_HMAC_KEY
          self.base_url = SPIKE_API_BASE_URL
          self.hmac_signature = self._generate_hmac_signature()
          self.access_token = None

      def _generate_hmac_signature(self):
          h = hmac.new(self.hmac_key.encode('utf-8'), self.user_id.encode('utf-8'), hashlib.sha256)
          return h.hexdigest()

      def get_bearer_token(self) -> str:
          if self.access_token is not None:
              return self.access_token
          else:
              body = {
                  "application_id": self.app_id,
                  "application_user_id": self.user_id,
                  "signature": self.hmac_signature
              }

              response = requests.post(
                  f"{self.base_url}/auth/hmac",
                  json=body,
                  headers={"Content-Type": "application/json", "Accept": "application/json"},
                  timeout=20
              )

              if response.status_code == 200:
                  try:
                      print(f"Successfully obtained Bearer token for {self.user_id}")
                      self.access_token = response.json()["access_token"]
                      return self.access_token
                  except (KeyError, json.JSONDecodeError) as e:
                      print(f"Failed to parse access token from successful response for {self.user_id}: {e}")
                  raise requests.RequestException(f"Token parsing failed for {self.user_id}")
              else:
                  raise Exception(f"Authentication failed for {self.user_id} with status {response.status_code}")

  class LabReportsAPI:
      def __init__(self, auth: SpikeAuth):
          self.spike_auth = auth

      def _create_auth_headers(self, method="GET") -> dict[str, str]:
          """Create authentication headers - see authentication docs for details"""
          try:
              token = self.spike_auth.get_bearer_token()
              req_headers = {
                  "Authorization": f"Bearer {token}",
                  "Accept": "application/json"
              }
              if method == "POST":
                  req_headers["Content-Type"] = "application/json"
              return req_headers
          except requests.RequestException as auth_error:
              print(f"Failed to get bearer token for Lab Reports API request header: {auth_error}")
          raise

      def upload_lab_report(self, document_path, **options):
          """Upload a lab report document for analysis"""
          # Read and encode document
          try:
              with open(document_path, 'rb') as doc_file:
                  document_data = base64.b64encode(doc_file.read()).decode('utf-8')
          except FileNotFoundError:
              print(f"Document file not found: {document_path}")
              return None

          # Prepare the request body
          body = {
              "body": document_data,
              "mime_type": options.get("mime_type", "application/pdf"),
              "filename": options.get("filename", "lab_report.pdf"),
              "wait_on_process": options.get("wait_on_process", False)
          }

          # Create request body and headers
          body_json = json.dumps(body)
          headers = self._create_auth_headers(method="POST")

          # Make request
          response = requests.post(
              f"{SPIKE_API_BASE_URL}/lab_reports",
              headers=headers,
              data=body_json
          )
          
          if response.status_code == 200:
              return response.json()

          print(f"Response status: {response.status_code}")
          return None

  # Example usage
  user_id = "lab_reports_test_user"
  spike_auth = SpikeAuth(user_id)
  api = LabReportsAPI(spike_auth)

  # Upload document asynchronously (recommended)
  result = api.upload_lab_report("/path/to/lab_report.pdf", wait_on_process=False)

  if result:
      print(f"Status: {result['lab_report']['status']}")
      print(f"Record ID: {result['lab_report']['record_id']}")

  # For synchronous processing (wait for results)
  result = api.upload_lab_report("/path/to/lab_report.pdf", wait_on_process=True)

  if result and result['lab_report']['status'] == 'completed':
      lab_report = result['lab_report']
      print(f"Analysis completed for {lab_report['record_id']}")
      print(f"Collection Date: {lab_report.get('collection_date', 'N/A')}")
      print(f"Result Date: {lab_report.get('result_date', 'N/A')}")
      
      if lab_report.get('sections'):
          print(f"Found {len(lab_report['sections'])} sections")
          for section in lab_report['sections']:
              print(f"- Section: {section.get('original_section_name', 'Unknown')}")
              for result in section.get('results', []):
                  value = result.get('value') or result.get('value_text', 'N/A')
                  unit = result.get('original_unit_name', '')
                  normal_status = ""
                  if result.get('within_normal_range') is not None:
                      normal_status = " (Normal)" if result['within_normal_range'] else " (Abnormal)"
                  print(f"  + {result.get('original_test_name')}: {value} {unit}{normal_status}")
  ```

  ```javascript [JavaScript] theme={null}
  const fs = require('fs');
  const axios = require('axios');

  class LabReportsAPI {
      constructor(applicationId, hmacKey, baseUrl = 'https://api.spikeapi.com') {
          this.applicationId = applicationId;
          this.hmacKey = hmacKey;
          this.baseUrl = baseUrl;
          this.accessToken = null;
      }

      async createAuthHeaders() {
          // Create authentication headers - see /api-docs/authentication for details
          if (!this.accessToken) {
              throw new Error('Access token not available. Call authenticate() first.');
          }
          return {
              'Authorization': `Bearer ${this.accessToken}`,
              'Content-Type': 'application/json'
          };
      }

      async authenticate(userId) {
          const crypto = require('crypto');
          
          // Generate HMAC signature
          const hmac = crypto.createHmac('sha256', this.hmacKey);
          hmac.update(userId);
          const signature = hmac.digest('hex');

          // Exchange signature for access token
          const response = await axios.post(`${this.baseUrl}/auth/hmac`, {
              application_id: this.applicationId,
              application_user_id: userId,
              signature: signature
          });

          this.accessToken = response.data.access_token;
          return this.accessToken;
      }

      async uploadLabReport(documentPath, options = {}) {
          // Read and encode document
          const documentBuffer = fs.readFileSync(documentPath);
          const documentBase64 = documentBuffer.toString('base64');

          // Prepare the request body
          const body = {
              body: documentBase64,
              mime_type: options.mimeType || 'application/pdf',
              filename: options.filename || 'lab_report.pdf',
              wait_on_process: options.waitOnProcess || false
          };

          const headers = await this.createAuthHeaders();

          // Make request
          try {
              const response = await axios.post(
                  `${this.baseUrl}/lab_reports`,
                  body,
                  { headers }
              );
              return response.data;
          } catch (error) {
              throw new Error(`API request failed: ${error.response?.data?.message || error.message}`);
          }
      }
  }

  // Example usage
  async function analyzeLabReport() {
      const api = new LabReportsAPI(
          9999, // your application ID
          'HMAC_KEY_FROM_ADMIN_CONSOLE' // your HMAC key
      );

      try {
          // Authenticate first
          await api.authenticate('my_application_user_123'); // your user ID

          // Asynchronous analysis (recommended)
          const result = await api.uploadLabReport('path/to/lab-report.pdf', {
              waitOnProcess: false
          });

          console.log(`Analysis started. Record ID: ${result.lab_report.record_id}`);
          console.log(`Status: ${result.lab_report.status}`);

          // Synchronous analysis (wait for results)
          const syncResult = await api.uploadLabReport('path/to/lab-report.pdf', {
              waitOnProcess: true
          });

          if (syncResult.lab_report.status === 'completed') {
              const labReport = syncResult.lab_report;
              console.log(`Collection Date: ${labReport.collection_date || 'N/A'}`);
              console.log(`Result Date: ${labReport.result_date || 'N/A'}`);
              
              labReport.sections?.forEach(section => {
                  console.log(`Section: ${section.original_section_name}`);
                  section.results?.forEach(result => {
                      const value = result.value || result.value_text || 'N/A';
                      const unit = result.original_unit_name || '';
                      const normalStatus = result.within_normal_range !== null ? 
                          (result.within_normal_range ? ' (Normal)' : ' (Abnormal)') : '';
                      console.log(`- ${result.original_test_name}: ${value} ${unit}${normalStatus}`);
                  });
              });
          }
      } catch (error) {
          console.error('Error:', error.message);
      }
  }

  analyzeLabReport();
  ```

  ```go [Go] theme={null}
  package main

  import (
      "bytes"
      "crypto/hmac"
      "crypto/sha256"
      "encoding/base64"
      "encoding/hex"
      "encoding/json"
      "fmt"
      "io"
      "net/http"
      "os"
      "time"
  )

  type LabReportsAPI struct {
      BaseURL       string
      ApplicationID int64
      HMACKey       string
      AccessToken   string
  }

  type UploadRequest struct {
      Body          string `json:"body"`
      MimeType      string `json:"mime_type"`
      Filename      string `json:"filename"`
      WaitOnProcess bool   `json:"wait_on_process"`
  }

  type LabReportResponse struct {
      LabReport LabReport `json:"lab_report"`
  }

  type LabReport struct {
      RecordID       string       `json:"record_id"`
      Status         string       `json:"status"`
      CollectionDate string       `json:"collection_date"`
      ResultDate     string       `json:"result_date"`
      Sections       []LabSection `json:"sections"`
      UploadedAt     time.Time    `json:"uploaded_at"`
  }

  type LabSection struct {
      LoincCode           string      `json:"loinc_code"`
      LoincCommonName     string      `json:"loinc_common_name"`
      OriginalSectionName string      `json:"original_section_name"`
      Results             []LabResult `json:"results"`
  }

  type LabResult struct {
      LoincCode          string   `json:"loinc_code"`
      LoincCommonName    string   `json:"loinc_common_name"`
      OriginalTestName   string   `json:"original_test_name"`
      OriginalUnitName   string   `json:"original_unit_name"`
      StandardUnitName   string   `json:"standard_unit_name"`
      Value              *float64 `json:"value,omitempty"`
      ValueText          *string  `json:"value_text,omitempty"`
      WithinNormalRange  *bool    `json:"within_normal_range,omitempty"`
      NormalMin          *float64 `json:"normal_min,omitempty"`
      NormalMax          *float64 `json:"normal_max,omitempty"`
      RequireHumanReview bool     `json:"require_human_review,omitempty"`
  }

  func NewLabReportsAPI(applicationID int64, hmacKey string) *LabReportsAPI {
      return &LabReportsAPI{
          BaseURL:       "https://api.spikeapi.com",
          ApplicationID: applicationID,
          HMACKey:       hmacKey,
      }
  }

  func (api *LabReportsAPI) Authenticate(userID string) error {
      // Generate HMAC signature
      h := hmac.New(sha256.New, []byte(api.HMACKey))
      h.Write([]byte(userID))
      signature := hex.EncodeToString(h.Sum(nil))

      // Prepare authentication request
      authBody := map[string]interface{}{
          "application_id":      api.ApplicationID,
          "application_user_id": userID,
          "signature":          signature,
      }

      bodyBytes, err := json.Marshal(authBody)
      if err != nil {
          return fmt.Errorf("failed to marshal auth request: %w", err)
      }

      // Make an authentication request
      req, err := http.NewRequest("POST", api.BaseURL+"/auth/hmac", bytes.NewBuffer(bodyBytes))
      if err != nil {
          return fmt.Errorf("failed to create auth request: %w", err)
      }

      req.Header.Set("Content-Type", "application/json")
      req.Header.Set("Accept", "application/json")

      client := &http.Client{Timeout: 30 * time.Second}
      resp, err := client.Do(req)
      if err != nil {
          return fmt.Errorf("auth request failed: %w", err)
      }
      defer resp.Body.Close()

      if resp.StatusCode != 200 {
          return fmt.Errorf("authentication failed with status %d", resp.StatusCode)
      }

      // Parse response
      respBody, err := io.ReadAll(resp.Body)
      if err != nil {
          return fmt.Errorf("failed to read auth response: %w", err)
      }

      var authResp map[string]string
      if err := json.Unmarshal(respBody, &authResp); err != nil {
          return fmt.Errorf("failed to parse auth response: %w", err)
      }

      api.AccessToken = authResp["access_token"]
      return nil
  }

  func (api *LabReportsAPI) createAuthHeaders() map[string]string {
      if api.AccessToken == "" {
          panic("Access token not available. Call Authenticate() first.")
      }
      return map[string]string{
          "Content-Type":  "application/json",
          "Authorization": "Bearer " + api.AccessToken,
      }
  }

  func (api *LabReportsAPI) UploadLabReport(documentPath string, options UploadRequest) (*LabReportResponse, error) {
      // Read and encode document
      documentData, err := os.ReadFile(documentPath)
      if err != nil {
          return nil, fmt.Errorf("failed to read document: %w", err)
      }
      
      options.Body = base64.StdEncoding.EncodeToString(documentData)
      
      // Set defaults
      if options.MimeType == "" {
          options.MimeType = "application/pdf"
      }
      if options.Filename == "" {
          options.Filename = "lab_report.pdf"
      }
      
      // Create a request body
      bodyBytes, err := json.Marshal(options)
      if err != nil {
          return nil, fmt.Errorf("failed to marshal request: %w", err)
      }
      
      // Create request
      req, err := http.NewRequest("POST", api.BaseURL+"/lab_reports", bytes.NewBuffer(bodyBytes))
      if err != nil {
          return nil, fmt.Errorf("failed to create request: %w", err)
      }
      
      // Set headers
      authHeaders := api.createAuthHeaders()
      for key, value := range authHeaders {
          req.Header.Set(key, value)
      }
      
      // Make request
      client := &http.Client{Timeout: 60 * time.Second}
      resp, err := client.Do(req)
      if err != nil {
          return nil, fmt.Errorf("request failed: %w", err)
      }
      defer resp.Body.Close()
      
      // Parse response
      respBody, err := io.ReadAll(resp.Body)
      if err != nil {
          return nil, fmt.Errorf("failed to read response: %w", err)
      }
      
      var result LabReportResponse
      if err := json.Unmarshal(respBody, &result); err != nil {
          return nil, fmt.Errorf("failed to parse response: %w", err)
      }
      
      return &result, nil
  }

  func main() {
      api := NewLabReportsAPI(9999, "HMAC_KEY_FROM_ADMIN_CONSOLE")
      
      // Authenticate first
      err := api.Authenticate("my_application_user_123")
      if err != nil {
          fmt.Printf("Authentication failed: %v\n", err)
          return
      }
      
      // Asynchronous analysis
      result, err := api.UploadLabReport("path/to/lab-report.pdf", UploadRequest{
          WaitOnProcess: false,
      })
      
      if err != nil {
          fmt.Printf("Error: %v\n", err)
          return
      }
      
      fmt.Printf("Analysis started. Record ID: %s\n", result.LabReport.RecordID)
      fmt.Printf("Status: %s\n", result.LabReport.Status)
      
      // Synchronous analysis
      syncResult, err := api.UploadLabReport("path/to/lab-report.pdf", UploadRequest{
          WaitOnProcess: true,
      })
      
      if err != nil {
          fmt.Printf("Error: %v\n", err)
          return
      }
      
      if syncResult.LabReport.Status == "completed" {
          labReport := syncResult.LabReport
          fmt.Printf("Collection Date: %s\n", labReport.CollectionDate)
          fmt.Printf("Result Date: %s\n", labReport.ResultDate)
          
          for _, section := range labReport.Sections {
              fmt.Printf("Section: %s\n", section.OriginalSectionName)
              for _, result := range section.Results {
                  value := "N/A"
                  if result.Value != nil {
                      value = fmt.Sprintf("%.2f", *result.Value)
                  } else if result.ValueText != nil {
                      value = *result.ValueText
                  }
                  fmt.Printf("- %s: %s", result.OriginalTestName, value)
                  if result.OriginalUnitName != "" {
                      fmt.Printf(" %s", result.OriginalUnitName)
                  }
                  if result.WithinNormalRange != nil {
                      if *result.WithinNormalRange {
                          fmt.Printf(" (Normal)")
                      } else {
                          fmt.Printf(" (Abnormal)")
                      }
                  }
                  fmt.Printf("\n")
              }
          }
      }
  }
  ```
</CodeGroup>

For complete API specification, data types, and additional parameters, see the [`POST /lab_reports`](/api-reference/lab-reports-upload-lab-report) API Reference.
