Skip to content
Outreach Desk

API guide

Version 1

The API lets your own software, or tools like Zapier and Make, read and add contacts in a Outreach Desk workspace and see what happened to them.

Keys

The workspace's owner or an admin makes keys in Settings → API & webhooks. A key either reads only, or reads and writes. It is shown once, when it's made, so copy it then. Send it with every request:

curl https://company-outreach-desk.vercel.app/api/v1/me \
  -H "Authorization: Bearer od_your_key"

Each key may make 120 requests a minute. Past that the API answers 429; wait a moment and try again. Delete a key you no longer use: anything still using it stops working at once.

Requests

RequestKeyWhat it does
GET /meRead onlyWhich workspace the key belongs to. A quick way to check a key works.
GET /contactsRead onlyContacts, newest first. ?limit (up to 100), ?page, ?q (searches email, company and name), ?tag, ?updated_since (a time in milliseconds).
POST /contactsRead & writeAdds a contact, or updates the one with the same email. { email, name, company, tags, fields }
GET /contacts/{id}Read onlyOne contact.
PATCH /contacts/{id}Read & writeChanges a contact. Any of { name, company, tags, fields, addTags, removeTags }.
DELETE /contacts/{id}Read & writeMoves a contact to the trash, where it stays for 30 days.
GET /campaignsRead onlyCampaigns with their status and counts.
POST /hooksRead & writeSubscribes an address to an event (for Zapier and Make). { target_url, event }
DELETE /hooks/{id}Read & writeUnsubscribes it.
GET /hooks/sampleRead onlyAn example of what a hook receives. ?event=reply
GET /eventsRead onlyWhat happened, newest first: sent, reply, bounce, click, open, unsubscribe, conversion and more. ?type=reply,click, ?since, ?limit (up to 200).

Bodies are JSON, sent with Content-Type: application/json. Custom fields are only saved if the workspace has a column with that key.

curl https://company-outreach-desk.vercel.app/api/v1/contacts \
  -H "Authorization: Bearer od_your_key" \
  -H "Content-Type: application/json" \
  -d '{"email":"sara@example.com","name":"Sara","company":"Desert Spa","tags":["webinar"]}'

A contact looks like this:

{
  "id": "sara-example-com", "email": "sara@example.com", "name": "Sara", "company": "Desert Spa",
  "tags": ["webinar"], "fields": {}, "status": "Not sent", "stage": "", "value": null,
  "assignee": "", "unsubscribed": false, "createdAt": 1793180000000, "updatedAt": 1793180000000
}

Webhooks

In Settings → API & webhooks add an address (it must start with https://) and choose the events you want: reply, click, bounce, unsubscribe, order and so on. When one happens we send a POST with a JSON body:

{
  "id": "6540f1c2...", "type": "reply", "createdAt": 1793180000000, "workspace": "your-workspace-id",
  "data": { "contactId": "sara-example-com", "contact": { "id": "sara-example-com", "email": "sara@example.com", "name": "Sara", "company": "Desert Spa" } }
}

Every message has these headers: X-OD-Event, X-OD-Delivery (its unique id: use it to skip a repeat), X-OD-Timestamp (seconds) and X-OD-Signature. To check a message came from us, compute an HMAC-SHA256 of timestamp + "." + the raw body with your signing secret (shown once when you add the webhook), write it in hex with v1= in front, and compare it with the signature. Reject messages whose timestamp is more than five minutes old.

import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret, headers, rawBody) {
  const age = Math.abs(Date.now() / 1000 - Number(headers["x-od-timestamp"]));
  const mine = "v1=" + createHmac("sha256", secret).update(headers["x-od-timestamp"] + "." + rawBody).digest("hex");
  const theirs = String(headers["x-od-signature"]);
  return age < 300 && mine.length === theirs.length && timingSafeEqual(Buffer.from(mine), Buffer.from(theirs));
}

Answer with any 2xx status within 10 seconds. Anything else, or no answer, is tried again after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, then given up on. We don't follow redirects. If five messages in a row are given up on, the webhook is switched off (you can switch it on again in Settings). Messages are sent at least once and may arrive out of order, and they can be a few minutes late because they go out with our background work.

Zapier and Make

Make a key that can write, then use the three pieces below. They follow the usual “REST hook” pattern, so a Zapier or Make app can be built on them: a trigger subscribes with POST /hooks when it's turned on, receives the webhook messages described above, and unsubscribes with DELETE /hooks/{id} when it's turned off. GET /hooks/sample gives the example shown while a person sets the step up, and GET /me checks the key. To add or update a contact from a Zap or scenario, use POST /contacts.

Until there's a published Zapier or Make app, you can already use both tools: in Zapier use “Webhooks by Zapier” (a catch hook as the trigger, and its address in a webhook here); in Make use the “Webhooks” and “HTTP” modules with your key.

Google Sheets, HubSpot and Pipedrive

These are connected in Settings → API & webhooks and need no code. Every few hours (and when you press Sync now) people come in from a published Google Sheet, your HubSpot contacts or your Pipedrive people, and are added or updated by email address: no one is deleted, and a blank cell never wipes what you already have. For HubSpot and Pipedrive, people who reply, book a meeting or buy are sent back (HubSpot: their lead status or lifecycle stage; Pipedrive: a note on the person). A sync reads up to 500 people from HubSpot or Pipedrive per run, carrying on from there next time, and up to 2,000 rows from a sheet.

Errors

Errors come back as { "error": "what went wrong" } with a status: 400 for a request that can't be used, 401 for a missing or wrong key, 403 when a read-only key tries to change something, 402 when the plan's limit is reached, 404 when there's nothing with that id, and 429 for too many requests.

Good to know

  • Adding a contact through the API doesn't email them. Whether they get emails is decided by your campaigns, sequences and automations, as for any contact.
  • Contacts who unsubscribed stay unsubscribed, whatever the API sends.
  • Times are milliseconds since 1 January 1970, in UTC.