Technical reference

Every request CaseDesk makes to your Google account

This page lists each Gmail call the tool will use, why it's needed, and exactly what is sent and received. Every item starts with a one-line plain English summary, with the technical detail underneath for anyone who wants it.

5Gmail API calls, all read only
$0 / monthRuns on Google Apps Script in your own account
Every 5 minHow often it checks for new emails

Permissions you'll be asked to approve

When you connect, Google shows a screen listing these. Each one is the narrowest option that does the job.

Google scopeWhat Google shows youWhy CaseDesk needs it
gmail.readonlyView your email messages and settingsTo find emails from your tracked senders and copy them into cases. It cannot send, delete, move or mark anything.Required
spreadsheets.currentonlySee, edit, create and delete only the spreadsheet this app is attached toYour cases live in one private Google Sheet. It can't open any of your other sheets.Required
drive.fileSee and manage only the Drive files this app createsSaves attachments into a "CaseDesk Attachments" folder. Your other Drive files stay invisible to it.Required
script.scriptappAllow this app to run when you are not presentLets the 5 minute timer check for new emails while your laptop is closed.Required
script.send_mailSend email as youOnly for the "new case" alert, sent to your own address. Skip email alerts and this permission isn't requested at all.Optional

How it runs, every 5 minutes

Here's the whole loop, in the order it happens. The calls in each step are detailed further down.

  1. 1
    Timer wakes up
    A time based Apps Script trigger fires. Nothing runs on any outside server.
  2. 2
    Ask Gmail what's new
    history.list from the last saved checkpoint. On first run it uses messages.list with a from: filter instead.
  3. 3
    Read each new email
    messages.get returns sender, subject, date, body and the list of attachments. Senders not on your list are skipped right here.
  4. 4
    New case or existing one?
    If the email's threadId already belongs to a case, it's added to that case's thread. Otherwise a new row gets the next number, like CASE-0007.
  5. 5
    Save attachments
    attachments.get, then the file is written to the Drive folder and linked on the case.
  6. 6
    Alert you
    The in-app bell updates, and an email alert goes to you if you've turned that on.

Gmail API calls

Base URL for all of these: https://gmail.googleapis.com/gmail/v1. In Apps Script they're called through the built-in Gmail Advanced Service, which signs every request with your approved permissions automatically.

GET users.getProfile

/users/me/profile

In plain words: confirms which Gmail account is connected, so the app can show "Connected as you@gmail.com".

When: once, right after you connectQuota cost: 1 unit
Request
GET /gmail/v1/users/me/profile
Authorization: Bearer <access_token>
Response 200
{
  "emailAddress": "you@gmail.com",
  "messagesTotal": 18452,
  "threadsTotal": 9311,
  "historyId": "4823907"
}
The historyId is saved as the starting checkpoint for step 2.

GET users.messages.list

/users/me/messages?q=from:(...) after:...

In plain words: searches your inbox for emails from the senders you track, the same way the Gmail search bar does.

When: first run, and as a backup if the checkpoint expiresQuota cost: 5 units
ParameterExamplePurpose
qfrom:(a@x.com OR b@y.com) after:1760572800Built from your Settings list, so changing senders needs no code
maxResults100Page size
pageToken0912...Next page, if there are more
Request
GET /gmail/v1/users/me/messages
  ?q=from:(claims@northfield-insurance.example
        OR support@harbourbank.example)
     after:1760572800
  &maxResults=100
Authorization: Bearer <access_token>
Response 200
{
  "messages": [
    { "id": "192f3a7c1b8e4d20", "threadId": "192f3a7c1b8e4d20" },
    { "id": "192f2c90ab31f7e5", "threadId": "192ef01d55c2a9b8" }
  ],
  "resultSizeEstimate": 2
}
In Apps Script:
const res = Gmail.Users.Messages.list('me', {
  q: 'from:(' + senders.join(' OR ') + ') after:' + lastCheck,
  maxResults: 100
});

GET users.history.list

/users/me/history?startHistoryId=...&historyTypes=messageAdded

In plain words: asks Gmail "what arrived since I last checked?" so the app never re-reads your whole inbox. This is what keeps every check fast.

When: every 5 minutesQuota cost: 2 units
Request
GET /gmail/v1/users/me/history
  ?startHistoryId=4823907
  &historyTypes=messageAdded
  &labelId=INBOX
Authorization: Bearer <access_token>
Response 200
{
  "history": [
    {
      "id": "4823951",
      "messagesAdded": [
        { "message": {
            "id": "192f4be01a77c3d9",
            "threadId": "192f3a7c1b8e4d20",
            "labelIds": ["INBOX", "UNREAD"]
        } }
      ]
    }
  ],
  "historyId": "4823988"
}
The new historyId replaces the saved checkpoint. If Google says the old checkpoint is too old (HTTP 404), the app falls back to messages.list for that run, so no email is missed.

GET users.messages.get

/users/me/messages/{id}?format=full

In plain words: opens one email and reads everything a case needs: who sent it, when, the subject, the full text, and which files are attached.

When: once per new emailQuota cost: 5 units
Request
GET /gmail/v1/users/me/messages/192f4be01a77c3d9
  ?format=full
Authorization: Bearer <access_token>
Response 200 (trimmed)
{
  "id": "192f4be01a77c3d9",
  "threadId": "192f3a7c1b8e4d20",
  "internalDate": "1760604512000",
  "snippet": "We have received your claim regarding...",
  "payload": {
    "mimeType": "multipart/mixed",
    "headers": [
      { "name": "From", "value": "Claims Team <claims@northfield-insurance.example>" },
      { "name": "Subject", "value": "Claim update needed for policy #44821" },
      { "name": "Date", "value": "Thu, 16 Oct 2026 14:08:32 +0530" },
      { "name": "Message-ID", "value": "<a81c@northfield-insurance.example>" }
    ],
    "parts": [
      { "partId": "0", "mimeType": "text/plain",
        "body": { "size": 412, "data": "SGVsbG8sDQoNCldlIGhhdmUg..." } },
      { "partId": "1", "mimeType": "application/pdf",
        "filename": "Claim_Form_44821.pdf",
        "body": { "attachmentId": "ANGjdJ8w...Qx", "size": 217088 } }
    ]
  }
}
How each field maps to a case: From becomes the sender, Subject the subject, internalDate the date received. The text/plain part (base64url decoded) is the full body, falling back to text/html stripped to text. threadId decides new case vs. existing case, and every part with a filename is an attachment.

GET users.messages.attachments.get

/users/me/messages/{messageId}/attachments/{attachmentId}

In plain words: downloads one attached file (PDF, image, Word doc) so a copy can be kept with the case.

When: once per attachmentQuota cost: 5 units
Request
GET /gmail/v1/users/me/messages/192f4be01a77c3d9
    /attachments/ANGjdJ8w...Qx
Authorization: Bearer <access_token>
Response 200
{
  "attachmentId": "ANGjdJ8w...Qx",
  "size": 217088,
  "data": "JVBERi0xLjcKJeLjz9MKNSAwIG9iago8PC9G..."
}

Other Google services

These two aren't Gmail calls. They're how attachments get stored and how you get notified.

POST Drive files.create

https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart

In plain words: saves the attachment into your "CaseDesk Attachments" Drive folder, named after the case, so it opens from the case page.

When: once per attachmentPermission: drive.file
Request (Apps Script)
const blob = Utilities.newBlob(
  Utilities.base64DecodeWebSafe(att.data),
  'application/pdf',
  'CASE-0007_Claim_Form_44821.pdf'
);
const file = folder.createFile(blob);
Result
{
  "id": "1Xq8vR2mT0bLkP4sWcZ9yHn3",
  "name": "CASE-0007_Claim_Form_44821.pdf",
  "mimeType": "application/pdf",
  "webViewLink": "https://drive.google.com/file/d/1Xq8.../view"
}

POST MailApp.sendEmail Optional

Apps Script built-in, sends from your account to your account

In plain words: emails you a short "New case CASE-0007" note with a link to open it. The recipient is fixed to your own address in Settings.

When: once per new case, only if alerts are onPermission: script.send_mail
Request (Apps Script)
MailApp.sendEmail({
  to: settings.alertEmail,            // your own address, from Settings
  subject: 'New case CASE-0007: Claim update needed for policy #44821',
  htmlBody: 'From claims@northfield-insurance.example<br>' +
            '<a href="' + appUrl + '#/case/CASE-0007">Open case</a>'
});
// Returns nothing. The alert shows up in your inbox within seconds.

CaseDesk app functions

These run between the CaseDesk screens and your Google Sheet. The web page calls them with google.script.run, and only you can reach them because the web app is set to "only me".

FN listCases

In plain words: loads the dashboard, newest case first, with counts per status.

Call
google.script.run
  .withSuccessHandler(render)
  .listCases({ limit: 50, offset: 0 });
Returns
{
  "counts": { "total": 7, "Open": 3, "In Progress": 2, "Closed": 2 },
  "cases": [
    {
      "caseNumber": "CASE-0007",
      "received": "2026-10-16T08:38:32Z",
      "sender": "claims@northfield-insurance.example",
      "subject": "Claim update needed for policy #44821",
      "issue": "",
      "status": "Open",
      "emailCount": 1
    }
  ]
}

FN getCase

In plain words: opens one case with its full email thread and attachment links.

Call
google.script.run
  .withSuccessHandler(showCase)
  .getCase('CASE-0007');
Returns
{
  "caseNumber": "CASE-0007",
  "threadId": "192f3a7c1b8e4d20",
  "status": "In Progress",
  "issue": "Water damage claim, kitchen",
  "notes": "Sent photos Monday.",
  "messages": [
    {
      "messageId": "192f4be01a77c3d9",
      "from": "claims@northfield-insurance.example",
      "date": "2026-10-16T08:38:32Z",
      "subject": "Claim update needed for policy #44821",
      "body": "Hello,\n\nWe have received your claim...",
      "attachments": [
        { "name": "Claim_Form_44821.pdf", "size": 217088,
          "driveUrl": "https://drive.google.com/file/d/1Xq8.../view" }
      ]
    }
  ]
}

FN updateCase

In plain words: saves your Issue, status and notes. Only these three fields can be edited, so the original email record stays untouched.

Call
google.script.run.updateCase('CASE-0007', {
  issue: 'Water damage claim, kitchen',
  status: 'In Progress',
  notes: 'Sent photos Monday. Waiting on estimate.'
});
Returns
{
  "ok": true,
  "caseNumber": "CASE-0007",
  "updatedAt": "2026-10-16T09:02:11Z"
}

// Invalid status:
{ "ok": false, "error": "Status must be Open, In Progress or Closed" }

FN searchCases

In plain words: the search page. Any mix of case number, keyword, issue, date range and status.

Call
google.script.run.searchCases({
  caseNumber: '',
  keyword: 'refund',     // subject, body, sender, notes
  issue: '',
  status: 'Closed',
  from: '2026-09-01',
  to: '2026-10-16'
});
Returns
{
  "count": 1,
  "cases": [
    { "caseNumber": "CASE-0003", "status": "Closed",
      "issue": "Refund for cancelled subscription",
      "received": "2026-10-05T11:20:00Z" }
  ]
}

FN getSettings / saveSettings

In plain words: the Settings page. Add or remove tracked senders and switch email alerts on or off. Changes apply on the next 5 minute check.

Call
google.script.run.saveSettings({
  senders: [
    'claims@northfield-insurance.example',
    'support@harbourbank.example'
  ],
  emailAlerts: true,
  alertEmail: 'you@gmail.com'
});
Returns
{
  "ok": true,
  "senders": ["claims@northfield-insurance.example", "support@harbourbank.example"],
  "emailAlerts": true
}

// Bad address:
{ "ok": false, "error": "\"harbourbank\" isn't a valid email address" }

Data model

Three tabs in one private Google Sheet. You can open it any time and see your data in plain rows, which doubles as a backup.

TabColumnExampleNotes
CasescaseNumberCASE-0007Unique, never reused
threadId192f3a7c1b8e4d20Gmail thread, used to group replies
received2026-10-16 14:08First email's date
senderclaims@northfield-insurance.example
subjectClaim update needed for policy #44821
issueWater damage claim, kitchenYou edit
statusIn ProgressOpen / In Progress / Closed
notesSent photos Monday.You edit
updatedAt2026-10-16 14:32
MessagesmessageId192f4be01a77c3d9Unique, stops duplicates
caseNumberCASE-0007Links to Cases
fromclaims@northfield-insurance.example
date2026-10-16 14:08
bodyHello, We have received...Full text
attachments[{"name":"Claim_Form_44821.pdf","driveId":"1Xq8..."}]Drive links
Settingskeysenders, emailAlerts, lastHistoryId, nextCaseNumber
valueclaims@northfield-insurance.example, support@harbourbank.example

Limits and cost

Google gives every account a free allowance. For a personal case tracker the usage is tiny.

ItemTypical useFree allowanceCost
Gmail API callsAbout 2 quota units per check, plus about 10 for each new email250 quota units per second, per user$0
Apps Script timer288 short runs a day (every 5 min), a few seconds each90 minutes of trigger runtime a day on a free Gmail account$0
Google SheetOne row per case and one per email10 million cells$0
Drive storageAttachments onlyShares your existing 15 GB Google storage$0
HostingApps Script serves the web appIncluded$0

What it never does

No deleting, moving or labelling

Calls like messages.modify, messages.trash and messages.delete are never used, and the read only permission blocks them anyway. Your inbox looks exactly the same as before.

No outside servers

Everything runs as Apps Script inside your Google account. No emails are copied to a third party database.

No push notifications from Google

users.watch would need a paid Google Cloud Pub/Sub setup. Checking every 5 minutes gives you nearly the same speed with zero setup and zero cost.

No access to other senders

Emails from addresses not on your list are skipped and never saved. You can remove a sender any time in Settings.