# Business Card Scanning

Source: https://docs.havilo.ai/reference/business-card-scan

> **Goal**
>
> This endpoint allows you to upload a picture and an Entity Type, and automatically populates all
> the entity's fields by extracting relevant information from the business card.

## Quick Example

Here's a simple example of scanning a business card using an API key:

```typescript
const response = await fetch('https://havilo.ai/api/entities/scan-business-card', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.HAVILO_API_KEY}`,
  },
  body: JSON.stringify({
    imageBase64: base64ImageData,
    mimeType: 'image/jpeg',
    entityTypeId: 'your-entity-type-id',
  }),
})

const result = await response.json()
console.log(result.extractedFields) // { Name: "John Smith", Email: "john@example.com", ... }
```

***

## Authentication

This endpoint requires API key authentication. See [API Authentication](https://docs.havilo.ai/reference/authentication.md) for details on creating and managing API keys.

```bash
Authorization: Bearer sk_live_abcdefgh.ijklmnopqrstuvwxyz...
```

**Requirements:**

* API key must have `ENTITY_CREATE` permission in its scopes
* For workspace-scoped keys, the `workspaceId` in the request body must match the key's workspace (or can be omitted)
* For system-level keys, `workspaceId` must be provided in the request body

### Authentication Errors

| Status Code | Error                      | Description                                        |
| ----------- | -------------------------- | -------------------------------------------------- |
| 401         | `Unauthorized`             | Invalid or missing API key                         |
| 403         | `Insufficient permissions` | API key lacks `ENTITY_CREATE` permission           |
| 403         | `Workspace mismatch`       | Provided workspaceId doesn't match API key's scope |

***

## Endpoint

`POST /api/entities/scan-business-card`

Extract structured contact information from a business card image.

### Request

**Content-Type**: `application/json`

### Request Body Parameters

All parameters are required:

| Parameter      | Type   | Required | Description                                                                                      |
| -------------- | ------ | -------- | ------------------------------------------------------------------------------------------------ |
| `imageBase64`  | string | **Yes**  | Base64 encoded image data **without** the data URL prefix (e.g., omit `data:image/jpeg;base64,`) |
| `mimeType`     | string | **Yes**  | Image MIME type: `image/jpeg`, `image/png`, `image/webp`, or `image/gif`                         |
| `entityTypeId` | string | **Yes**  | ID of the entity type that defines which fields to extract                                       |
| `workspaceId`  | string | **Yes**  | Workspace ID for permission validation and entity type lookup                                    |

### Image Requirements

> **Supported Formats**
>
> **File Types**: JPEG, PNG, WebP, GIF
>
> **Recommended Size**: Under 5MB for optimal performance
>
> **Quality**: Higher resolution images produce better extraction results
>
> **Orientation**: Any orientation supported (portrait or landscape)

### Example Request

```json
{
  "imageBase64": "/9j/4AAQSkZJRgABAQAAAQABAAD...",
  "mimeType": "image/jpeg",
  "entityTypeId": "4d15d6af-f5c8-4126-bef5-9f4321f1d598",
  "workspaceId": "c33f6347-e9a4-4acc-a3df-d0c74c736dbd"
}
```

***

## Response

### Success Response (200 OK)

When the business card is successfully scanned, you'll receive:

```json
{
  "success": true,
  "extractedFields": {
    "Name": "Jane Doe",
    "Email": "jane.doe@example.com",
    "Phone": "(555) 123-4567",
    "Company": "Acme Corporation",
    "Title": "Senior Product Manager"
  },
  "fieldsFound": ["Name", "Email", "Phone", "Company", "Title"],
  "confidence": 0.92,
  "validation": {
    "valid": true,
    "errors": []
  }
}
```

#### Response Fields

| Field               | Type      | Description                                                                                     |
| ------------------- | --------- | ----------------------------------------------------------------------------------------------- |
| `success`           | boolean   | Always `true` for successful requests                                                           |
| `extractedFields`   | object    | Key-value pairs of extracted data. Only includes fields found on the card with non-empty values |
| `fieldsFound`       | string\[] | Array of field names that were successfully extracted                                           |
| `confidence`        | number    | AI confidence score from 0 to 1 (default: 0.7 if not provided by AI)                            |
| `validation.valid`  | boolean   | Whether all extracted fields passed validation rules                                            |
| `validation.errors` | string\[] | Array of validation error messages (empty if all fields are valid)                              |

> **Dynamic Field Extraction**
>
> The extracted fields are determined by your entity type configuration. The API adapts to your
> custom field definitions, so different entity types will extract different fields.

### Error Responses

#### 400 Bad Request - Missing Parameters

```json
{
  "error": "Missing required fields: imageBase64, mimeType, entityTypeId, workspaceId"
}
```

One or more required parameters are missing from the request body.

#### 400 Bad Request - Invalid File Type

```json
{
  "error": "Invalid file type. Only images are supported."
}
```

The provided `mimeType` is not an image format.

#### 401 Unauthorized

```json
{
  "error": "Unauthorized"
}
```

Invalid or missing API key. Ensure you're passing a valid `Authorization: Bearer` header.

#### 403 Forbidden

```json
{
  "error": "Insufficient permissions",
  "required": ["ENTITY_CREATE"],
  "mode": "all"
}
```

API key lacks the required `ENTITY_CREATE` permission in its scopes.

#### 404 Not Found

```json
{
  "error": "Entity type not found"
}
```

The specified `entityTypeId` doesn't exist or isn't accessible in the workspace.

#### 400 Bad Request - No Fields

```json
{
  "error": "Entity type has no fields to extract"
}
```

The entity type has no field definitions configured.

#### 500 Internal Server Error

```json
{
  "error": "Failed to scan business card",
  "message": "AI service unavailable"
}
```

An unexpected error occurred during processing (e.g., AI service timeout, parsing error).

***

## Field Validation

The API automatically validates extracted data based on field types of the corresponding entity type schema:

| Field Type  | Validation Rule                                                    | Auto-Fix                                |
| ----------- | ------------------------------------------------------------------ | --------------------------------------- |
| `EMAIL`     | Must contain `@` symbol                                            | None                                    |
| `URL`       | Must be a valid URL                                                | Prepends `https://` if protocol missing |
| `NUMBER`    | Must be numeric                                                    | None                                    |
| `BOOLEAN`   | Must be: `true`, `false`, `1`, `0`, `yes`, `no` (case-insensitive) | None                                    |
| Other types | No validation                                                      | None                                    |

> **Validation Doesn't Block Success**
>
> Validation errors don't prevent a 200 OK response. Failed validations are returned in the
> `validation.errors` array, allowing you to decide how to handle them.

### Example Response with Validation Errors

```json
{
  "success": true,
  "extractedFields": {
    "Name": "Bob Johnson",
    "Email": "not-a-valid-email"
  },
  "fieldsFound": ["Name", "Email"],
  "confidence": 0.65,
  "validation": {
    "valid": false,
    "errors": ["Field \"Email\" should be a valid email"]
  }
}
```

***

## Code Examples

**TypeScript**

```typescript
// Store your API key securely (e.g., environment variables)
const API_KEY = process.env.HAVILO_API_KEY

async function scanBusinessCard(
  imageFile: File,
  entityTypeId: string,
  workspaceId?: string // Optional if using workspace-scoped API key
) {
  // Convert image to base64
  const base64Data = await new Promise<string>((resolve) => {
    const reader = new FileReader()
    reader.onloadend = () => {
      const result = reader.result as string
      // Remove data URL prefix
      const base64 = result.split(',')[1]
      resolve(base64)
    }
    reader.readAsDataURL(imageFile)
  })

  // Make API request with API key authentication
  const response = await fetch('https://havilo.ai/api/entities/scan-business-card', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${API_KEY}`,
    },
    body: JSON.stringify({
      imageBase64: base64Data,
      mimeType: imageFile.type,
      entityTypeId,
      ...(workspaceId && { workspaceId }), // Only include if provided
    }),
  })

  if (!response.ok) {
    const error = await response.json()
    throw new Error(error.error || 'Failed to scan business card')
  }

  return await response.json()
}

// Usage with API key (workspace-scoped key - workspaceId optional)
try {
  const result = await scanBusinessCard(
    file,
    'clx1234567890'
  )

  console.log('Extracted fields:', result.extractedFields)
  console.log('Confidence:', result.confidence)

  if (!result.validation.valid) {
    console.warn('Validation errors:', result.validation.errors)
  }
} catch (error) {
  console.error('Scan failed:', error.message)
}
```

**Python**

```python
import os
import requests
import base64
from typing import Dict, Any, Optional

# Load API key from environment
API_KEY = os.getenv('HAVILO_API_KEY')

def scan_business_card(
    image_path: str,
    entity_type_id: str,
    workspace_id: Optional[str] = None  # Optional for workspace-scoped keys
) -> Dict[str, Any]:
    """
    Scan a business card image and extract contact information.

    Args:
        image_path: Path to the business card image file
        entity_type_id: Entity type ID for field extraction
        workspace_id: Workspace ID (optional for workspace-scoped API keys)

    Returns:
        Dictionary containing extracted fields and metadata
    """
    # Read and encode image
    with open(image_path, 'rb') as image_file:
        image_data = base64.b64encode(image_file.read()).decode('utf-8')

    # Determine MIME type
    mime_type = 'image/jpeg'
    if image_path.lower().endswith('.png'):
        mime_type = 'image/png'
    elif image_path.lower().endswith('.webp'):
        mime_type = 'image/webp'
    elif image_path.lower().endswith('.gif'):
        mime_type = 'image/gif'

    # Build request body
    body = {
        'imageBase64': image_data,
        'mimeType': mime_type,
        'entityTypeId': entity_type_id,
    }
    if workspace_id:
        body['workspaceId'] = workspace_id

    # Make API request with API key authentication
    headers = {
        'Authorization': f'Bearer {API_KEY}',
        'Content-Type': 'application/json',
    }

    response = requests.post(
        'https://havilo.ai/api/entities/scan-business-card',
        json=body,
        headers=headers
    )

    response.raise_for_status()
    return response.json()

# Usage with workspace-scoped API key (workspaceId optional)
try:
    result = scan_business_card(
        'business-card.jpg',
        'clx1234567890'
    )

    print('Extracted fields:', result['extractedFields'])
    print('Confidence:', result['confidence'])

    if not result['validation']['valid']:
        print('Validation errors:', result['validation']['errors'])

except requests.exceptions.HTTPError as error:
    print('Scan failed:', error.response.json())
```

**cURL**

```bash
# Set your API key as an environment variable
export HAVILO_API_KEY="sk_live_abcdefgh.ijklmnopqrstuvwxyz..."

# First, encode your image to base64
IMAGE_BASE64=$(base64 -i business-card.jpg | tr -d '\n')

# Make the API request with API key authentication
curl -X POST "https://havilo.ai/api/entities/scan-business-card" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $HAVILO_API_KEY" \
  -d "{
    \"imageBase64\": \"$IMAGE_BASE64\",
    \"mimeType\": \"image/jpeg\",
    \"entityTypeId\": \"c33f6347-e9a4-4acc-a3df-d0c74c736dbd\"
  }"
```

***

## Best Practices

### 1. Always Check Confidence Scores

```typescript
if (result.confidence < 0.7) {
  // Low confidence - require user review
  showReviewDialog(result.extractedFields)
} else {
  // High confidence - safe to auto-populate
  populateForm(result.extractedFields)
}
```

### 2. Handle Validation Errors Gracefully

```typescript
if (!result.validation.valid) {
  result.validation.errors.forEach((error) => {
    console.warn(error)
    // Highlight the problematic field in your UI
  })
}
```

### 3. Optimize Image Quality

```typescript
// Compress large images before sending
async function optimizeImage(file: File): Promise<File> {
  if (file.size > 2 * 1024 * 1024) {
    // 2MB
    // Resize or compress the image
    return await compressImage(file, { maxWidth: 2000, quality: 0.8 })
  }
  return file
}
```

***

## Data Privacy

> **No Storage**
>
> Business card images are **not stored** on our servers. Images are processed immediately and
> discarded after extraction completes.

***

## Need Help?

If you encounter issues or have questions:

* Check [Entities](https://docs.havilo.ai/concepts/entities.md) to understand entity types and field configuration
* Review your API key scopes to ensure `ENTITY_CREATE` permission is granted
* Verify your entity type has field definitions configured
* See [API Authentication](https://docs.havilo.ai/reference/authentication.md) for API key setup
