Vouchfield

API REFERENCE

The Vouchfield API

Start website audits and read their results from your own systems, Zapier or Make, and get a signed message when an audit finishes.

API access and webhooks are part of the Agency plan.

Getting started

The API lets your own systems, Zapier or Make start website audits for your workspace and read the results. Webhooks send your system a signed message when an audit finishes, so you do not have to keep asking.

  1. The workspace owner opens Settings → API keys and makes a key. It is shown once, so copy it straight away.
  2. Check the key works by asking who you are:
curl https://vouchfield.com/v1/me \
  -H "Authorization: Bearer YOUR_API_KEY"

Every address in this reference starts with https://vouchfield.com. Requests and answers are JSON. Times are UTC.

Your API key

Send the key in the Authorization header of every request, after the word Bearer and a space:

Authorization: Bearer YOUR_API_KEY
  • Main key. Can do everything below, including making and switching off extra keys, setting the webhook address and connecting Slack. Keep it for yourself.
  • Extra keys. Up to 25 switched on at once, each with its own name. They can start and read audits through the API, and use your workspace credits. They don't open the workspace pages: a teammate signs in with their own email instead. Give each tool its own key, so you can switch one off or replace it without touching the others.
  • Keys are made, replaced and switched off by the workspace owner in Settings → API keys. We keep only a fingerprint of each key, so a lost key is replaced, never recovered.
  • API access and webhooks are part of the Agency plan. GET /v1/me and GET /v1/plan work on every plan, so a key can always see why it was refused.
  • Never put a key in a web page, an email or a shared document.

Speed limit

Each key can make 120 requests a minute (counted over the last 60 seconds). The main key and each extra key have their own allowance.

  • Beyond it the answer is 429 with error.code rate_limited and a Retry-After header: the number of whole seconds to wait.
  • Every answer to a request with a key has X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until there is room again).
  • Requests with a key we do not recognise are limited to 30 a minute from each network address.
  • Credits are separate: each audit uses one credit from your plan, and your plan has a daily limit. When credits run out the answer is 429 with error.code limit_reached, and nothing is charged for that request.
  • Request bodies can be up to 16 KB of JSON.

Endpoints

Replace YOUR_API_KEY, AUDIT_ID and the other capitals with your own values. Where an example says YOUR_MAIN_API_KEY, only the main key works.

GET /v1/me

Your workspace, this key, usage and credits. Works on every plan.

Answers 200 OK when it works.

curl https://vouchfield.com/v1/me \
  -H "Authorization: Bearer YOUR_API_KEY"

GET /v1/plan

Your plan, monthly credits and daily limit. Works on every plan.

Answers 200 OK when it works.

curl https://vouchfield.com/v1/plan \
  -H "Authorization: Bearer YOUR_API_KEY"

POST /v1/scan

Start a website audit. Uses one credit. Send at least url. The answer (202) has the audit id, a status_url to read it and a report_url. The audit runs in the background: read it with GET /v1/scan/{id}, list finished ones with GET /v1/scans, or get a webhook message when it finishes. Anyone with report_url can open the report. Send "private": true to keep it to your workspace, then share it with a client link.

FieldWhereWhat it is
Idempotency-KeyHeaderSend the same value again (for example after a timeout) and you get the same audit back instead of a new one, with no second credit.
urlBody, requiredThe website to audit, for example example.com.
nameBody, optionalThe business name, if you know it. Helps match its Google listing.
townBody, optionalThe business town, if you know it.
stBody, optionalTwo-letter US state code, for example NJ.
profileBody, optionalAn audit template id from your workspace.
business_typeBody, optionalOne of auto, local, service_area, software, online. auto (the default) works it out from the website.
webhook_urlBody, optionalMain key only. Saves the workspace's webhook address (https only).
privateBody, optionaltrue keeps the report to your workspace (share it with a client link). Without it, anyone with report_url can open the report.

Answers 202 Accepted when it works.

curl -X POST https://vouchfield.com/v1/scan \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: crm-row-1042" \
  -d '{"url": "example.com", "name": "Example Bakery", "town": "Newark", "st": "NJ"}'

GET /v1/scan/{id}

One audit with every check. status is queued, running, done or failed. A finished audit has the score, the areas, the top fixes and every check (each pass, warn, fail or skip; a check that does not apply is skip and does not change the score). started_at and finished_at are UTC; finished_at is empty while the audit runs.

FieldWhereWhat it is
idIn the pathThe audit id you got when you started it.

Answers 200 OK when it works.

curl https://vouchfield.com/v1/scan/AUDIT_ID \
  -H "Authorization: Bearer YOUR_API_KEY"

GET /v1/scans

Your audits, most recently changed first, a page at a time (for polling).

FieldWhereWhat it is
statusIn the addressOnly audits with this status.
siteIn the addressOnly audits of this website, for example example.com (subdomains included).
limitIn the addressPage size.
cursorIn the addressnext_cursor from the previous page.

Answers 200 OK when it works.

curl "https://vouchfield.com/v1/scans?status=done&limit=25" \
  -H "Authorization: Bearer YOUR_API_KEY"

# the next page: send next_cursor back as cursor, with the same filters
curl "https://vouchfield.com/v1/scans?status=done&limit=25&cursor=NEXT_CURSOR" \
  -H "Authorization: Bearer YOUR_API_KEY"

GET /v1/seats

Your extra API keys, oldest first, a page at a time. An extra key sees only itself. The main key sees every extra key. A whole key is never shown again, only how it starts.

FieldWhereWhat it is
statusIn the addressOnly keys in this state.
limitIn the addressPage size.
cursorIn the addressnext_cursor from the previous page.

Answers 200 OK when it works.

curl https://vouchfield.com/v1/seats \
  -H "Authorization: Bearer YOUR_MAIN_API_KEY"

POST /v1/seats

Make an extra API key (main key only; at most 25 switched on). The answer has api_key: the whole new key, shown once. Copy it straight away.

FieldWhereWhat it is
labelBody, optionalA name for the key, up to 60 characters, for example Zapier.

Answers 201 Created when it works.

curl -X POST https://vouchfield.com/v1/seats \
  -H "Authorization: Bearer YOUR_MAIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"label": "Zapier"}'

DELETE /v1/seats/{id}

Switch off an extra API key (main key only).

FieldWhereWhat it is
idIn the pathThe id of the extra key, from GET /v1/seats.

Answers 200 OK when it works.

curl -X DELETE https://vouchfield.com/v1/seats/KEY_ID \
  -H "Authorization: Bearer YOUR_MAIN_API_KEY"

POST /v1/integrations/slack

Post finished-audit notes to a Slack incoming webhook (main key only; an empty url disconnects).

FieldWhereWhat it is
urlBody, requiredYour Slack incoming webhook address. Send an empty url to disconnect.

Answers 200 OK when it works.

curl -X POST https://vouchfield.com/v1/integrations/slack \
  -H "Authorization: Bearer YOUR_MAIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "YOUR_SLACK_INCOMING_WEBHOOK_ADDRESS"}'

GET /v1/openapi.json

This description of the API. Import it into Postman or another API tool. No key needed.

Answers 200 OK when it works.

curl https://vouchfield.com/v1/openapi.json

GET /v1/docs

This API reference, as a web page. No key needed.

Answers 200 OK when it works.

Pages of results

Lists (GET /v1/scans and GET /v1/seats) come a page at a time. Each answer has has_more and next_cursor. To get the next page, send next_cursor back as cursor, with the same filters. On the last page next_cursor is null.

  • A page can hold fewer items than limit, even none, while next_cursor is set. Keep following it.
  • GET /v1/scans lists audits by when they last changed, newest first. An audit that changes while you page may move to the top, so use its id to skip repeats.

Safe retries

When you start an audit with POST /v1/scan, add an Idempotency-Key header with a value that is unique to that job, for example the id of the row or record it came from (up to 100 characters).

  • If your request times out and you send it again with the same key, you get the same audit back (202, with the header Idempotent-Replayed: true) and no second credit is used, even at your daily limit.
  • Sending the same key with a different url is refused with 409. Changed settings are not applied to the first audit.

Errors

Every error is JSON of one shape:

{"detail": "A sentence you can show a person.",
 "error": {"status": 404, "code": "not_found", "message": "A sentence you can show a person."}}

error.code is a fixed word your program can check. detail is the same sentence, except for 422, where it lists the fields that were missing or not valid.

Statuserror.codeWhat it means
400bad_requestThe request is not valid.
401unauthorizedMissing or invalid API key.
403forbiddenThis API key cannot do that.
404not_foundNot found.
405method_not_allowedThat method is not allowed here.
409conflictThe request conflicts with the current state.
413too_largeThe request is too large.
415unsupported_media_typeSend the body as JSON with Content-Type: application/json.
422invalid_requestSome fields are missing or not valid.
429limit_reachedA limit was reached.
500server_errorSomething went wrong on our side. Try again later.
502bad_gatewayA service we rely on did not answer. Try again later.
503unavailableThe service is not available right now.
403plan_limitYour plan does not include this. The answer also has upgrade, the page to change plan.
429rate_limitedToo many requests a minute. Wait the seconds in Retry-After.

Webhooks

When an audit for your workspace finishes, we send a POST with a JSON body to your webhook address. It does not matter how the audit was started: in the workspace, through your website widget or through the API.

  1. The workspace owner opens Settings → Webhooks, pastes the address and saves. The address must be a public https:// address.
  2. Tick the events you want, then click Send a test message.
  3. With your main key you can also set the address with webhook_url when you start an audit. If messages were switched off after failed deliveries, this does not turn them back on: do that in Settings → Webhooks.

Events

EventWhen it is sent
scan.completedAn audit finished. The body is the same JSON as GET /v1/scan/{id} (status done).
scan.failedAn audit failed. The body is the same JSON as GET /v1/scan/{id} with status failed and failure_reason; credit_returned is true when we could not reach the website and gave the credit back. Sent only when the owner ticks this event in Settings → Webhooks.
webhook.testA test message the owner sends from Settings → Webhooks. Always the same small body.

A test message always has this body:

{"event": "webhook.test", "test": true, "message": "This is a test message from Vouchfield. It is not an audit."}

Headers

HeaderWhat it means
X-Audit-EventWhat happened: scan.completed when an audit finishes, scan.failed when an audit fails (only if you tick it in Settings → Webhooks), or webhook.test for a test message you send from Settings → Webhooks.
X-Audit-IdAn id for this message. It stays the same when we retry it, so you can skip repeats. A message you send again yourself from the delivery log gets a new id.
X-Audit-TimestampWhen the message was sent, in seconds since 1970.
X-Audit-SignatureA signature made with your agency's signing secret. See the note on checking messages below.

Answering and retries

  • Answer with a 2xx status within 10 seconds, as soon as you have stored the message. Do the slow work afterwards.
  • Anything else counts as a failure. We try again after about 1 minute, 5 minutes, 25 minutes and 2 hours (5 tries in all), then stop. Every retry keeps the same X-Audit-Id, so you can skip a message you already have.
  • If 3 messages in a row fail every try, we switch messages off and email the workspace owner. Fix the address, then turn them back on in Settings → Webhooks.
  • Redirects are not followed. Settings → Webhooks shows every try with the status your address answered, and can send a finished message again.

Checking a webhook message

Each message is signed with your workspace's signing secret, so you can check it came from us. The owner can reveal or replace the secret in Settings → Webhooks. It is not your API key, and a replaced secret stops working at once.

  1. Read the raw body exactly as it arrived, as bytes, before any JSON parsing.
  2. Refuse the message if X-Audit-Timestamp is more than 300 seconds (5 minutes) old, or more than 300 seconds in the future.
  3. Join the timestamp, a full stop and the raw body: timestamp + "." + raw_body.
  4. Make an HMAC-SHA256 of that with your signing secret and write it as lowercase hex. Put v1= in front.
  5. Compare it with X-Audit-Signature using a constant-time comparison.
  6. Skip a message whose X-Audit-Id you have already stored.

A retry is signed again with a fresh timestamp, so it is never refused by the 5-minute window.

In Python

import hashlib, hmac, time

def is_from_vouchfield(secret, raw_body, headers):
    ts = headers.get("X-Audit-Timestamp", "")
    sig = headers.get("X-Audit-Signature", "")
    if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
        return False
    mac = hmac.new(secret.encode(), ts.encode() + b"." + raw_body, hashlib.sha256)
    return hmac.compare_digest("v1=" + mac.hexdigest(), sig)

In Node

const crypto = require('crypto')

function isFromVouchfield(secret, rawBody, ts, header) {
  const mac = 'v1=' + crypto.createHmac('sha256', secret).update(ts + '.').update(rawBody).digest('hex')
  return Math.abs(Date.now() / 1000 - Number(ts)) <= 300 && header.length === mac.length &&
         crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(header))
}

Zapier's and Make's catch hooks cannot check a signature. There, treat the message as a tip-off and fetch the audit with GET /v1/scan/{id} and your key before acting on it. The Zapier and Make guide shows each step.

Machine-readable description

The same endpoints as an OpenAPI 3 file, for Postman and other API tools: /v1/openapi.json. No key needed.

Questions? Ask us.