Put new orders in a spreadsheet with the DMU Mail API

A short script reads each new conversation in orders@ and adds a row. Written in JavaScript, with the same calls in Python.

DMU Mail team· · 3 min read
Put new orders in a spreadsheet with the DMU Mail API
On this page

Orders arrive by email, and somebody copies them into a spreadsheet. A script can do that copying. It asks the DMU Mail API for the conversations in your orders mailbox, skips the ones it has already seen, and adds a row for each new one.

In short

  • Two API calls: list your mailboxes to find orders@, then list its conversations.

  • The key needs two permissions, and nothing more.

  • Run it every ten minutes or so. It adds each conversation once.

orders@aegeanfreight.example

GET /threads

Your script

one row each

orders.csv

The script writes a CSV file, which any spreadsheet program opens. To fill a hosted spreadsheet instead, swap the last step for that service's own call.

1. Make a key with two permissions

An administrator opens Settings › API keys and chooses Create a key. Name it after what will use it, such as "orders to spreadsheet". Instead of a preset, choose Custom and tick just:

  • List mailboxes (mailboxes:list), to find the mailbox's id;

  • List conversations, and search (threads:list), to read its conversations.

Copy the key when it is shown. It is shown once. Keep it in an environment variable, DMU_API_KEY, and never in the script.

A key reaches every mailbox its owner can

Permissions narrow what a key may do, but a key can't be limited to one mailbox. It sees what the administrator who made it sees. The script picks orders@ by its address, and the key's name tells your colleagues what it is for. If the owner is removed from the organisation, the key stops working.

2. The script

Each call sends the key as Authorization: Bearer … to https://app.dmu.email/api/v1. The script finds the mailbox, reads up to five pages of 50 conversations from its inbox, and keeps the ones it hasn't seen. Each row has the time of the newest message, the sender's address and name, the subject, and a line of preview text. The first run adds what is in the inbox now.

import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'

const API = 'https://app.dmu.email/api/v1'
const MAILBOX = 'orders@aegeanfreight.example'
const SEEN = 'seen.json'
const SHEET = 'orders.csv'

async function get(path, params = {}) {
  const url = new URL(API + path)
  for (const [name, value] of Object.entries(params)) if (value) url.searchParams.set(name, value)
  const response = await fetch(url, { headers: { Authorization: `Bearer ${process.env.DMU_API_KEY}` } })
  if (!response.ok) throw new Error(`${response.status} ${(await response.json()).error?.code}`)
  return response.json()
}

const cell = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`

const { data: mailboxes } = await get('/mailboxes')
const mailbox = mailboxes.find((m) => m.email === MAILBOX)
if (!mailbox) throw new Error(`This key can't see ${MAILBOX}`)

const seen = new Set(existsSync(SEEN) ? JSON.parse(readFileSync(SEEN, 'utf8')) : [])
const fresh = []
let cursor
for (let page = 0; page < 5; page++) {
  const { data, nextCursor } = await get('/threads', { mailbox: mailbox.id, view: 'inbox', limit: '50', cursor })
  const unseen = data.filter((t) => !seen.has(t.id))
  fresh.push(...unseen)
  if (!unseen.length || !nextCursor) break
  cursor = nextCursor
}

if (!existsSync(SHEET)) appendFileSync(SHEET, 'Received,From,Name,Subject,Preview\n')
for (const t of fresh.reverse()) {
  const row = [t.lastMessageAt, t.from?.address, t.from?.name, t.subject, t.preview]
  appendFileSync(SHEET, row.map(cell).join(',') + '\n')
  seen.add(t.id)
}
writeFileSync(SEEN, JSON.stringify([...seen]))
console.log(`${fresh.length} new`)

3. Run it on a schedule

DMU Mail checks each mailbox every few minutes, so polling more than once a minute gains nothing. A key may make 120 requests a minute, and this script makes two or three per run.

*/10 * * * * cd /path/to/orders && DMU_API_KEY="dmk_…" node orders.mjs

Notes

  • The list is newest first and pages with nextCursor, which you pass back unchanged as cursor. Pagination explains it.

  • If a request is refused, the answer has an error with a code. missing_scope means the key lacks a permission, and the X-Required-Scope header names it. After a rate_limited, wait for the Retry-After seconds. Errors lists them.

  • Want the order number from the message text? Give the key threads:read as well, and ask Get a thread for each new conversation. The message bodies are written by the sender, so treat them as untrusted text.

  • To be told rather than to poll, use a webhook on message.received.

  • The API docs cover every other call.

Share

LinkedIn

Be one of the first teams in

We let a few organisations into the private beta every week. Join the waitlist and we’ll email you when there’s room for your team.

Join the waitlist

Join in three quick questions, or ask us to show you around. No card needed.