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

# Freshservice Integration

> Keep Freshservice asset records accurate automatically — Forager pushes confirmed locations into Freshservice every time a tech scans a device

When a field tech scans a biomedical device or IT asset, Forager confirms their physical location matches the asset's CMDB record. This guide connects that confirmation event directly to Freshservice — so every successful scan updates the asset's location in Freshservice automatically, with no manual entry.

**Choose this approach if** your organization uses Freshservice for IT Asset Management and wants physical location data to stay current without extra admin work.

## What happens

```
Tech scans BIO-4721 in Biomedical Storage (Forager app)
  → Attestation recorded in Forager
  → Forager POSTs to your proxy endpoint
  → Proxy looks up the asset by tag in Freshservice
  → Asset updated: last_discovered_at, location, confirmed_by
```

The update happens within seconds of the scan — before the tech has left the room.

***

## Freshservice setup

### Step 1 — Create a dedicated API user

Create a Freshservice agent account that the integration will authenticate as. Using a dedicated account makes it easy to audit integration activity and revoke access without affecting human users.

1. Go to **Admin → Agents → New Agent**
2. Set:
   * **Full name:** `Forager Integration`
   * **Email:** `forager@yourcompany.com` (a real address you control)
   * **Role:** `IT Agent` (minimum required for asset writes)
3. Save and copy the agent's **API key** from **Profile Settings → API Key**

<Warning>
  Do not use an admin account for the integration. The IT Agent role is sufficient and limits the blast radius if credentials are ever compromised.
</Warning>

### Step 2 — Create custom fields on your asset type

Freshservice's native `location` field is a hierarchical reference — it doesn't accept freeform text. The simplest path is to add four custom fields to store Forager data directly.

1. Go to **Admin → Asset Management → Asset Types**
2. Select your primary asset type (e.g. `Computer`, `Medical Device`)
3. Go to the **Fields** tab → **Add Field**
4. Add these four fields:

| Field label          | Field type       | Notes                                                                    |
| -------------------- | ---------------- | ------------------------------------------------------------------------ |
| Forager Location     | Single-line text | Full breadcrumb, e.g. `Memorial Hospital / Floor 3 / Biomedical Storage` |
| Forager Room         | Single-line text | Room name only, e.g. `Biomedical Storage`                                |
| Forager Confirmed By | Single-line text | Display name of the tech who scanned                                     |
| Forager Result       | Single-line text | `match`, `mismatch_confirmed`, `mismatch_dismissed`, or `new_asset`      |

Note the **field name** (not label) for each — it will look like `forager_location`, `forager_room`, etc. You'll need these exact names in the proxy.

<Tip>
  If you use Freshservice's native `location` field on a per-asset basis, you can omit `Forager Location` and instead have the proxy do a location lookup by name. This adds complexity — the four custom fields approach is simpler and works with Freshservice's standard plan.
</Tip>

***

## Proxy deployment

The proxy is a lightweight serverless function. It sits between Forager and Freshservice, receives the webhook payload, looks up the asset by tag, and calls the Freshservice API to update the record.

### Option A — Deploy to Vercel (recommended, free tier is sufficient)

1. Create a new directory and initialize a project:

```bash theme={null}
mkdir forager-freshservice-proxy && cd forager-freshservice-proxy
npm init -y
```

2. Create `api/forager.js`:

```js theme={null}
export default async function handler(req, res) {
  // Validate the shared secret Forager sends
  const secret = req.headers['x-forager-secret']
  if (secret !== process.env.SHARED_SECRET) {
    return res.status(401).json({ error: 'Unauthorized' })
  }

  const { asset_tag, last_discovered_at, location, room, discovered_by, discovery_type } = req.body

  if (!asset_tag) {
    return res.status(400).json({ error: 'asset_tag is required' })
  }

  const FS_DOMAIN  = process.env.FS_DOMAIN   // yourcompany.freshservice.com
  const FS_API_KEY = process.env.FS_API_KEY
  const auth = Buffer.from(`${FS_API_KEY}:X`).toString('base64')
  const fsHeaders = {
    Authorization: `Basic ${auth}`,
    'Content-Type': 'application/json',
  }

  // 1. Find the asset by tag
  const searchRes = await fetch(
    `https://${FS_DOMAIN}/api/v2/assets?search[asset_tag]=${encodeURIComponent(asset_tag)}`,
    { headers: fsHeaders }
  )
  if (!searchRes.ok) {
    console.error('Freshservice search failed:', searchRes.status, await searchRes.text())
    return res.status(502).json({ error: 'Freshservice search failed' })
  }
  const { assets } = await searchRes.json()
  if (!assets?.length) {
    // Log but return 200 so Forager doesn't retry — this asset may not be in Freshservice yet
    console.warn('Asset not found in Freshservice:', asset_tag)
    return res.status(200).json({ skipped: true, reason: 'asset_not_found' })
  }

  const displayId = assets[0].display_id

  // 2. Update the asset
  const updateRes = await fetch(`https://${FS_DOMAIN}/api/v2/assets/${displayId}`, {
    method: 'PUT',
    headers: fsHeaders,
    body: JSON.stringify({
      asset: {
        last_discovered_at,
        custom_fields: {
          forager_location:     location,
          forager_room:         room,
          forager_confirmed_by: discovered_by,
          forager_result:       discovery_type,
        },
      },
    }),
  })

  if (!updateRes.ok) {
    const text = await updateRes.text()
    console.error('Freshservice update failed:', updateRes.status, text)
    return res.status(502).json({ error: 'Freshservice update failed', detail: text })
  }

  return res.status(200).json({ ok: true, display_id: displayId })
}
```

3. Create `.env` (for local testing only — never commit this):

```bash theme={null}
FS_DOMAIN=yourcompany.freshservice.com
FS_API_KEY=your_api_key_here
SHARED_SECRET=choose_a_random_string_here
```

4. Deploy to Vercel:

```bash theme={null}
npx vercel --prod
```

Copy the production URL (e.g. `https://forager-freshservice-proxy.vercel.app`).

5. Set environment variables in the Vercel dashboard: **Project → Settings → Environment Variables**. Add `FS_DOMAIN`, `FS_API_KEY`, and `SHARED_SECRET`.

### Option B — AWS Lambda (Node.js 20.x)

Use the same handler code above. Set the three environment variables in the Lambda function's configuration. Use an **API Gateway HTTP API** trigger and copy the invoke URL as your webhook endpoint.

***

## Forager configuration

1. Go to **Dashboard → Settings → Webhooks** → **Add Webhook**
2. Fill in:

| Field                 | Value                                          |
| --------------------- | ---------------------------------------------- |
| **Name**              | `Freshservice`                                 |
| **Endpoint URL**      | Your proxy URL + `/api/forager`                |
| **Target Platform**   | `Freshservice`                                 |
| **Auth Header Name**  | `X-Forager-Secret`                             |
| **Auth Header Value** | The `SHARED_SECRET` value you set in the proxy |

3. Click **Add Webhook**. The webhook fires on every attestation from this point forward.

***

## Testing

Send a test payload to your proxy to verify the Freshservice update works before relying on real scan data:

```bash theme={null}
curl -X POST "https://your-proxy.vercel.app/api/forager" \
  -H "Content-Type: application/json" \
  -H "X-Forager-Secret: your_shared_secret" \
  -d '{
    "event": "forager.attestation",
    "asset_tag": "BIO-4721",
    "last_discovered_at": "2026-06-07T14:23:11Z",
    "location": "Memorial Hospital / Floor 3 / Biomedical Storage",
    "room": "Biomedical Storage",
    "discovered_by": "Maria Ortega",
    "discovery_type": "match"
  }'
```

**Expected response:**

```json theme={null}
{ "ok": true, "display_id": 42 }
```

Then open the asset in Freshservice and confirm:

* `Last discovered at` is updated
* `Forager Location` custom field shows `Memorial Hospital / Floor 3 / Biomedical Storage`
* `Forager Confirmed By` shows `Maria Ortega`

***

## Troubleshooting

| Symptom                                        | Likely cause                            | Fix                                                                                                                  |
| ---------------------------------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
| Proxy returns `401`                            | `X-Forager-Secret` doesn't match        | Verify the secret in Forager's webhook config matches `SHARED_SECRET` in the proxy                                   |
| Proxy returns `skipped: asset_not_found`       | Asset tag doesn't exist in Freshservice | Check the asset tag in both systems — they must match exactly, including case                                        |
| `502` on search                                | `FS_API_KEY` or `FS_DOMAIN` wrong       | Test the key directly: `curl -u "API_KEY:X" "https://DOMAIN/api/v2/assets?per_page=1"`                               |
| `502` on update                                | Custom field names wrong                | Check the field names on the Freshservice asset type — they must match the `custom_fields` keys in the proxy exactly |
| Fields update but `last_discovered_at` doesn't | Freshservice plan limitation            | `last_discovered_at` updates require the Asset Management add-on on some Freshservice plans                          |
| Webhook fires but proxy logs nothing           | Proxy URL is wrong                      | Test the URL with curl; check Vercel/Lambda logs for incoming requests                                               |

***

## What your team gains

Every scan a biomedical or IT tech performs while working a ticket automatically updates Freshservice — with no dedicated inventory walk. After 30 days of normal field operations, the vast majority of active assets will have current, attestation-backed location data in Freshservice. Before your next Joint Commission or ISO 55001 audit, export the Freshservice asset list instead of scheduling a manual walkdown.
