Skip to main content

About

The Spike SDK provides a convenient interface for the Nutrition AI API, allowing you to analyze food images directly from your Android application. The SDK handles image encoding, API communication, and response parsing, making it easy to integrate nutritional analysis into your app.
All Spike SDK suspending methods should be called from a coroutine scope and wrapped in try-catch blocks. See Error Handling for details.

Key Features

  • AI-Powered Analysis — advanced computer vision for food identification and nutritional calculations
  • Flexible Processing — choose between synchronous (wait for results) or asynchronous (background) processing
  • Bitmap Support — convenient methods that accept Bitmap directly, in addition to base64-encoded strings
  • Complete Record Management — retrieve, update, and delete nutrition records

Available Methods

MethodDescription
analyzeNutrition(image, consumedAt, config)Submit food image for synchronous processing and wait for the analysis results
submitNutritionForAnalysis(image, consumedAt, config)Submit food image for asynchronous processing and get record ID immediately for polling afterwards
getNutritionRecords(from, to)Retrieve nutrition records for a datetime range
getNutritionRecord(id)Get a specific nutrition record by ID
updateNutritionRecordServingSize(id, servingSize)Update serving size for a nutrition record
deleteNutritionRecord(id)Delete a nutrition record by ID

Analyzing Food Images

Synchronous Processing

Use synchronous analysis when you want to wait for the complete nutritional analysis before proceeding. This is ideal for scenarios where you need immediate results and can display a loading indicator.
import com.spikeapi.apiv3.SpikeConnectionAPIv3
import com.spikeapi.apiv3.datamodels.*
import java.time.Instant

// image: Bitmap - captured from camera or photo library
val image: Bitmap = // ... captured from camera or gallery

try {
    val record = spikeConnection.analyzeNutrition(
        image = image,
        consumedAt = Instant.now(),
        config = NutritionalAnalysisConfig(
            analysisMode = NutritionRecordAnalysisMode.PRECISE,
            countryCode = null,
            languageCode = null,
            includeNutriScore = true,
            includeDishDescription = true,
            includeIngredients = true,
            includeNutritionFields = listOf(
                NutritionalField.ENERGY_KCAL,
                NutritionalField.PROTEIN_G,
                NutritionalField.FAT_TOTAL_G,
                NutritionalField.CARBOHYDRATE_G
            )
        )
    )
    
    println("Dish: ${record.dishName ?: "Unknown"}")
    println("Serving size: ${record.servingSize ?: 0} ${record.unit?.value ?: "g"}")
    println("Calories: ${record.nutritionalFields?.get("energy_kcal") ?: 0}")
} catch (e: Exception) {
    println("Analysis failed: ${e.message}")
}
You can also use base64-encoded image data:
// Using base64-encoded string
val outputStream = ByteArrayOutputStream()
image.compress(Bitmap.CompressFormat.JPEG, 80, outputStream)
val base64String = Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP)

val record = spikeConnection.analyzeNutrition(
    imageBase64 = base64String,
    consumedAt = Instant.now(),
    config = null  // Uses default configuration
)
Processing Time: Synchronous processing takes some time depending on image complexity. Consider showing a loading indicator to users. If you see that the analysis is taking too long, the recommendation is to use asynchronous processing instead.

Asynchronous Processing

Use asynchronous processing when you want an immediate response without waiting for the analysis to complete. Record ID is returned. The image is processed in the background, and you can retrieve results later by requesting nutrition analysis using the record ID or receive them via webhook.
try {
    // Submit image for background processing
    val recordId: UUID = spikeConnection.submitNutritionForAnalysis(
        image = image,
        consumedAt = Instant.now(),
        config = NutritionalAnalysisConfig(
            analysisMode = NutritionRecordAnalysisMode.FAST,
            countryCode = null,
            languageCode = null,
            includeNutriScore = null,
            includeDishDescription = null,
            includeIngredients = true,
            includeNutritionFields = null
        )
    )
    
    println("Analysis started. Record ID: $recordId")
    
    // Optionally, poll for results later
    // Your backend will also receive a webhook when analysis completes
    
} catch (e: Exception) {
    println("Failed to submit: ${e.message}")
}

Retrieving Results Asynchronously

After submitting an image for asynchronous processing, you can retrieve the results using the record ID. Check the processing status for completion success.
// Check the status and get results
val record = spikeConnection.getNutritionRecord(id = recordId)
if (record != null) {
    when (record.status) {
        NutritionRecordStatus.COMPLETED -> {
            println("Analysis complete: ${record.dishName ?: "Unknown"}")
        }
        NutritionRecordStatus.PROCESSING -> {
            println("Still processing...")
        }
        NutritionRecordStatus.PENDING -> {
            println("Queued for processing...")
        }
        NutritionRecordStatus.FAILED -> {
            println("Analysis failed: ${record.failureReason ?: "Unknown error"}")
        }
        NutritionRecordStatus.UNKNOWN -> {
            println("Unknown status")
        }
    }
}
For real-time notifications, configure webhooks in your admin console. Your backend will receive a webhook notification when the analysis completes. See Asynchronous Processing for webhook implementation details.

Configuration Options

Customize the analysis using NutritionalAnalysisConfig:
val config = NutritionalAnalysisConfig(
    // Analysis speed vs. precision
    analysisMode = NutritionRecordAnalysisMode.PRECISE,  // PRECISE (default) or FAST
    
    // Country ISO 3166-1 alpha-2 code in lowercase
    countryCode = "us",

    // Language ISO 639-1 code in lowercase
    languageCode = "en",
    
    // Include Nutri-Score rating (A-E)
    includeNutriScore = true,
    
    // Include dish description
    includeDishDescription = true,
    
    // Include detailed breakdown of ingredients
    includeIngredients = true,
    
    // Specify which nutritional fields to include (using NutritionalField enum)
    includeNutritionFields = listOf(
        NutritionalField.ENERGY_KCAL,
        NutritionalField.PROTEIN_G,
        NutritionalField.FAT_TOTAL_G,
        NutritionalField.CARBOHYDRATE_G,
        NutritionalField.FIBER_TOTAL_DIETARY_G,
        NutritionalField.SODIUM_MG
    )
)

NutritionalAnalysisConfig

data class NutritionalAnalysisConfig(
    /** A preferred mode for the analysis. Default is PRECISE. */
    val analysisMode: NutritionRecordAnalysisMode?,
    /** Country ISO 3166-1 alpha-2 code in lowercase */
    val countryCode: String?,
    /** Language ISO 639-1 code in lowercase */
    val languageCode: String?,
    /** Include nutri-score label of the food. Default is false. */
    val includeNutriScore: Boolean?,
    /** Include dish description of the food. Default is false. */
    val includeDishDescription: Boolean?,
    /** Include ingredients of the food. Default is false. */
    val includeIngredients: Boolean?,
    /** 
     * Include specific nutrition fields in the analysis report.
     * By default, carbohydrate_g, energy_kcal, fat_total_g and protein_g will be included.
     */
    val includeNutritionFields: List<NutritionalField>?
)

Analysis Modes

enum class NutritionRecordAnalysisMode(val value: String) {
    FAST("fast"),
    PRECISE("precise")
}
ModeDescription
PRECISEUses advanced AI models for highest accuracy and detailed analysis (default)
FASTUses optimized models for quicker processing with good accuracy

Default Nutritional Fields

If includeNutritionFields is not specified, only these basic fields are included:
  • ENERGY_KCAL
  • PROTEIN_G
  • FAT_TOTAL_G
  • CARBOHYDRATE_G
See Nutritional Fields Reference for all available fields or check the API Reference for Kotlin enum values.

Managing Nutrition Records

List Records by Date Range

Retrieve all nutrition records within a specified date range:
import java.time.Instant
import java.time.temporal.ChronoUnit

val endDate = Instant.now()
val startDate = endDate.minus(7, ChronoUnit.DAYS)

try {
    val records = spikeConnection.getNutritionRecords(
        from = startDate,
        to = endDate
    )
    
    for (record in records) {
        val consumedAt = record.consumedAt?.toString() ?: "Unknown date"
        val size = record.servingSize ?: 0.0
        val unit = record.unit?.value ?: "g"
        println("$consumedAt: ${record.dishName ?: "Unknown"} - $size$unit")
    }
} catch (e: Exception) {
    println("Failed to fetch records: ${e.message}")
}

Get a Specific Record

Retrieve a single nutrition record by its ID:
try {
    val record = spikeConnection.getNutritionRecord(id = recordId)
    if (record != null) {
        println("Dish: ${record.dishName ?: "Unknown"}")
        println("Nutri-Score: ${record.nutriScore ?: "N/A"}")
        
        // Access nutritional values
        record.nutritionalFields?.get("energy_kcal")?.let { calories ->
            println("Calories: $calories kcal")
        }
        
        // Access ingredients if included
        record.ingredients?.forEach { ingredient ->
            println("- ${ingredient.name}: ${ingredient.servingSize}${ingredient.unit.value}")
        }
    } else {
        println("Record not found")
    }
} catch (e: Exception) {
    println("Failed to fetch record: ${e.message}")
}

Update Serving Size

Adjust the serving size of an existing record. All nutritional values are automatically recalculated proportionally:
try {
    val updatedRecord = spikeConnection.updateNutritionRecordServingSize(
        id = recordId,
        servingSize = 200.0  // New serving size in grams
    )
    
    println("Updated serving size: ${updatedRecord.servingSize ?: 0}${updatedRecord.unit?.value ?: "g"}")
    println("Recalculated calories: ${updatedRecord.nutritionalFields?.get("energy_kcal") ?: 0}")
} catch (e: Exception) {
    println("Failed to update record: ${e.message}")
}

Delete a Record

Permanently remove a nutrition record (success status is returned regardless record is found or not):
try {
    spikeConnection.deleteNutritionRecord(id = recordId)
    println("Record deleted successfully")
} catch (e: Exception) {
    println("Failed to delete record: ${e.message}")
}

Response Data

NutritionRecord

The NutritionRecord data class contains the analysis results:
data class NutritionRecord(
    /** Report record ID */
    val recordId: UUID,
    /** Processing status */
    val status: NutritionRecordStatus,
    /** Detected dish name */
    val dishName: String?,
    /** Detected dish description */
    val dishDescription: String?,
    /** Dish name translated to target language */
    val dishNameTranslated: String?,
    /** Dish description translated to target language */
    val dishDescriptionTranslated: String?,
    /** Nutri-Score known as the 5-Colour Nutrition label (A-E) */
    val nutriScore: String?,
    /** Reason for processing failure */
    val failureReason: String?,
    /** Serving size in metric units */
    val servingSize: Double?,
    /** Metric unit (g for solids, ml for liquids) */
    val unit: NutritionalUnit?,
    val nutritionalFields: Map<String, Double>?,
    /** List of detected ingredients with nutritional information */
    val ingredients: List<NutritionRecordIngredient>?,
    /** Upload timestamp in UTC */
    val uploadedAt: Instant,
    /** Update timestamp in UTC */
    val modifiedAt: Instant,
    /** The UTC time when food was consumed */
    val consumedAt: Instant?
)

NutritionRecordStatus

enum class NutritionRecordStatus(val value: String) {
    PENDING("pending"),
    PROCESSING("processing"),
    COMPLETED("completed"),
    FAILED("failed"),
    UNKNOWN("_unknown") // Unknown value was sent from API. SDK should be updated to use the newest API responses.
}

NutritionalUnit

enum class NutritionalUnit(val value: String) {
    G("g"),       // grams
    MG("mg"),     // milligrams
    MCG("mcg"),   // micrograms
    ML("ml"),     // milliliters
    KCAL("kcal"), // kilocalories
    UNKNOWN("_unknown") // Unknown value was sent from API. SDK should be updated to use the newest API responses.
}

NutritionRecordIngredient

data class NutritionRecordIngredient(
    /** Ingredient name using LANGUAL standard terminology */
    val name: String,
    /** Ingredient name translated to target language */
    val nameTranslated: String?,
    /** Serving size in metric units */
    val servingSize: Double,
    /** Metric unit (g for solids, ml for liquids) */
    val unit: NutritionalUnit,
    val nutritionalFields: Map<String, Double>?
)

NutritionalField

Use this enum to specify which nutritional fields to include in the analysis:
enum class NutritionalField(val value: String) {
    ENERGY_KCAL("energy_kcal"),
    CARBOHYDRATE_G("carbohydrate_g"),
    PROTEIN_G("protein_g"),
    FAT_TOTAL_G("fat_total_g"),
    FAT_SATURATED_G("fat_saturated_g"),
    FAT_POLYUNSATURATED_G("fat_polyunsaturated_g"),
    FAT_MONOUNSATURATED_G("fat_monounsaturated_g"),
    FAT_TRANS_G("fat_trans_g"),
    FIBER_TOTAL_DIETARY_G("fiber_total_dietary_g"),
    SUGARS_TOTAL_G("sugars_total_g"),
    CHOLESTEROL_MG("cholesterol_mg"),
    SODIUM_MG("sodium_mg"),
    POTASSIUM_MG("potassium_mg"),
    CALCIUM_MG("calcium_mg"),
    IRON_MG("iron_mg"),
    MAGNESIUM_MG("magnesium_mg"),
    PHOSPHORUS_MG("phosphorus_mg"),
    ZINC_MG("zinc_mg"),
    VITAMIN_ARAE_MCG("vitamin_a_rae_mcg"),
    VITAMIN_CMG("vitamin_c_mg"),
    VITAMIN_DMCG("vitamin_d_mcg"),
    VITAMIN_EMG("vitamin_e_mg"),
    VITAMIN_KMCG("vitamin_k_mcg"),
    THIAMIN_MG("thiamin_mg"),
    RIBOFLAVIN_MG("riboflavin_mg"),
    NIACIN_MG("niacin_mg"),
    VITAMIN_B6MG("vitamin_b6_mg"),
    FOLATE_MCG("folate_mcg"),
    VITAMIN_B12MCG("vitamin_b12_mcg")
}

Error Handling

All nutrition methods are suspending functions that can throw exceptions. Always wrap calls in try-catch blocks:
import com.spikeapi.SpikeExceptions

try {
    val record = spikeConnection.analyzeNutrition(
        image = image,
        consumedAt = Instant.now(),
        config = null
    )
    // Handle success
} catch (e: SpikeExceptions.SpikeException) {
    println("Spike error: ${e.message}")
} catch (e: SpikeExceptions.NetworkException) {
    println("Network error: ${e.message}")
} catch (e: SpikeExceptions.AuthenticationException) {
    println("Authentication failed: ${e.message}")
} catch (e: Exception) {
    println("Unexpected error: ${e.message}")
}

Common Error Scenarios

ErrorCause
Invalid image formatImage is not JPEG, PNG, or WebP
Image too largeBase64-encoded image exceeds 10MB
Image too smallImage is smaller than 512×512 pixels
UnauthorizedInvalid or expired authentication token
Analysis timeoutAI processing took too long
UnidentifiableNon-food image

Image Guidelines

For optimal analysis results, guide your users to capture images that:
  1. Center the food — capture the plate contents as the main subject
  2. Fill the frame — ensure the meal occupies most of the image
  3. Use proper lighting — natural or bright lighting works best
  4. Avoid obstructions — remove packaging and minimize utensils in frame
  5. Skip filters — avoid filters that alter the food’s appearance
See Image Guidelines for complete recommendations.

Best Practices

1. Request Only What You Need

Each additional field, ingredient breakdown, or optional data increases processing time. Only request what your app actually uses:
// ❌ Don't request everything "just in case"
val config = NutritionalAnalysisConfig(
    analysisMode = null,
    countryCode = null,
    languageCode = null,
    includeNutriScore = true,
    includeDishDescription = true,
    includeIngredients = true,
    includeNutritionFields = NutritionalField.entries  // All 29 fields
)

// ✅ Request only what you need
val config = NutritionalAnalysisConfig(
    includeDishDescription = true,
    includeNutritionFields = listOf(
        NutritionalField.ENERGY_KCAL
    )
)

2. Consider your actual UI requirements:

  • Do you display ingredients? If not, skip includeIngredients.
  • Do you show Nutri-Score? If not, skip includeNutriScore.
  • Which nutritional values do you actually display? Request only those.

3. Choose the Right Processing Mode

  • Synchronous (analyzeNutrition): Use when you need immediate results and can show a loading state
  • Asynchronous (submitNutritionForAnalysis): Use for better UX when you don’t need immediate results, or when processing multiple images

4. Handle All Status Values

When using asynchronous processing, always check the record status before accessing results:
val record = spikeConnection.getNutritionRecord(id = recordId)
if (record == null) {
    // Handle not found
    return
}

if (record.status != NutritionRecordStatus.COMPLETED) {
    if (record.status == NutritionRecordStatus.FAILED) {
        // Handle failure
        println("Failed: ${record.failureReason}")
    } else {
        // Still processing
        println("Status: ${record.status}")
    }
    return
}
// Safe to access results

5. Implement Webhook Handling

For production apps using asynchronous processing, implement webhook handling on your backend to receive real-time notifications when analysis completes.

6. Cache Configuration

Create a shared configuration object if you’re using the same settings across your app:
object NutritionConfig {
    val standard = NutritionalAnalysisConfig(
        analysisMode = NutritionRecordAnalysisMode.PRECISE,
        countryCode = null,
        languageCode = null,
        includeNutriScore = true,
        includeDishDescription = null,
        includeIngredients = true,
        includeNutritionFields = listOf(
            NutritionalField.ENERGY_KCAL,
            NutritionalField.PROTEIN_G,
            NutritionalField.FAT_TOTAL_G,
            NutritionalField.CARBOHYDRATE_G,
            NutritionalField.FIBER_TOTAL_DIETARY_G
        )
    )
}

// Usage
val record = spikeConnection.analyzeNutrition(
    image = image,
    consumedAt = Instant.now(),
    config = NutritionConfig.standard
)