KLIQ|Developers

Build Your Own WineScan

This recipe walks through creating a fully private WineScan vertical from scratch: registering the vertical, defining a custom grading schema (Parker 100-point + bottle condition), adding catalog items, and running an inspection.

What you will build

A private wine inspection product with a custom Parker-style 100-point flavor schema and a bottle condition schema, populated with private Bordeaux catalog items, ready to grade bottles via the inspection API.

1. Create the vertical

import { KliqClient } from '@kliq-ai/sdk';

const kliq = new KliqClient({ apiKey: process.env.KLIQ_API_KEY! });

const vertical = await kliq.verticals.create('t_abc123', {
  slug: 'acme-wine-scan',
  name: 'WineScan',
  description: 'Authentication, condition grading and provenance for fine wine.',
  brandColor: '#722F37',
});

console.log(vertical.id); // "vert_wine001"

2. Define grading schemas

Parker 100-point flavor schema

const parkerSchema = await kliq.gradingSchemas.create('t_abc123', {
  verticalSlug: 'acme-wine-scan',
  slug: 'parker-100-point',
  name: 'Parker 100-Point Scale',
  standard: 'Robert Parker / Wine Advocate',
  overallFormula: { method: 'weighted_average', weightField: 'weight' },
  attributes: [
    {
      key: 'color',
      name: 'Color & Appearance',
      type: 'scale',
      config: { min: 0, max: 5, step: 1 },
      weight: 0.05,
      required: true,
    },
    {
      key: 'aroma',
      name: 'Aroma & Bouquet',
      type: 'scale',
      config: { min: 0, max: 15, step: 1 },
      weight: 0.15,
      required: true,
    },
    {
      key: 'flavor',
      name: 'Flavor & Finish',
      type: 'scale',
      config: { min: 0, max: 20, step: 1 },
      weight: 0.20,
      required: true,
    },
    {
      key: 'overall_quality',
      name: 'Overall Quality',
      type: 'scale',
      config: { min: 0, max: 10, step: 1 },
      weight: 0.10,
      required: true,
    },
  ],
});

Bottle condition schema

const bottleSchema = await kliq.gradingSchemas.create('t_abc123', {
  verticalSlug: 'acme-wine-scan',
  slug: 'wine-bottle-condition',
  name: 'Wine Bottle Condition',
  standard: 'Auction House',
  overallFormula: { method: 'weighted_average', weightField: 'weight' },
  attributes: [
    {
      key: 'fill_level',
      name: 'Fill Level',
      type: 'enum',
      config: { options: ['IN', 'BN', 'VTS', 'TS', 'HS', 'MS', 'LS'] },
      weight: 0.4,
      required: true,
    },
    {
      key: 'label_condition',
      name: 'Label Condition',
      type: 'scale',
      config: { min: 1, max: 5, step: 1 },
      weight: 0.2,
      required: true,
    },
    {
      key: 'capsule',
      name: 'Capsule',
      type: 'enum',
      config: { options: ['intact', 'corroded', 'cut', 'seepage', 'missing'] },
      weight: 0.2,
      required: true,
    },
    {
      key: 'cork',
      name: 'Cork',
      type: 'enum',
      config: { options: ['sound', 'raised', 'sunken', 'damp', 'crumbly'] },
      weight: 0.2,
      required: true,
    },
  ],
});

console.log('Schemas created:', parkerSchema.slug, bottleSchema.slug);

3. Add catalog items

const tenant = kliq.forTenant('t_abc123');

// Add Penfolds Grange as a private catalog item under the public wine type
// (or create a private type first if the item doesn't fit the public catalog)
const grange = await kliq.catalog.items.create('t_abc123', {
  typeSlug: 'new-world-icons',  // from the public collector-scan catalog
  slug: 'penfolds-grange-2016',
  name: 'Penfolds Grange 2016',
  description: 'South Australian Shiraz-dominant flagship. Rated 99 points (Parker).',
  attributes: {
    producer: 'Penfolds',
    vintage: 2016,
    variety: 'Shiraz',
    region: 'South Australia',
    format: '750ml',
  },
});

const latour = await kliq.catalog.items.create('t_abc123', {
  typeSlug: 'bordeaux-first-growths',
  slug: 'chateau-latour-2005',
  name: 'Château Latour 2005',
  description: 'Grand Vin, Pauillac. Legendary vintage, 100-point scores from multiple critics.',
  attributes: {
    appellation: 'Pauillac',
    vintage: 2005,
    classification: '1er Grand Cru Classé 1855',
  },
});

console.log('Catalog items added:', grange.slug, latour.slug);

4. Run a bottle inspection

// Create the inspection for a physical bottle
const inspection = await tenant.inspections.create({
  catalogItemSlug: 'chateau-latour-2005',
  gradingSchemaSlug: bottleSchema.slug,  // assess physical condition first
  notes: 'OWC. Stored in temperature-controlled cellar since release.',
  metadata: { cellarRef: 'CELLAR-A-42', owner: 'collector_pierre' },
});

// Upload bottle photos (label, capsule, fill level, cork if opened)
const { uploadUrl, imageUrl } = await tenant.observations.getUploadUrl({
  filename: 'latour-2005-label.jpg',
  contentType: 'image/jpeg',
});

await fetch(uploadUrl, { method: 'PUT', body: bottleLabelImage, headers: { 'Content-Type': 'image/jpeg' } });

await tenant.observations.create({
  locationId: 'loc_cellar_a',
  imageUrl,
  metadata: { inspectionId: inspection.id, angle: 'label' },
});

// Trigger AI grading
await tenant.inspections.grade(inspection.id);

5. Review the results

// Poll until graded
let result = await tenant.inspections.get(inspection.id);
while (result.status === 'grading') {
  await new Promise(r => setTimeout(r, 2000));
  result = await tenant.inspections.get(inspection.id);
}

console.log('AI condition grades:', result.aiGrades);
// {
//   fill_level:      'BN',       // Base of Neck — excellent
//   label_condition: 5,          // Pristine
//   capsule:         'intact',
//   cork:            'sound',
// }

// Finalize with human confirmation
const final = await tenant.inspections.submitHumanGrades(inspection.id, {
  grades: result.aiGrades,  // all confirmed
  notes: 'Perfect condition. Original wooden case present.',
});

console.log('Status:', final.status); // "completed"

Next steps