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"API REFERENCE
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.
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.
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.
Send the key in the Authorization header of every request, after the word Bearer and a space:
Authorization: Bearer YOUR_API_KEYGET /v1/me and GET /v1/plan work on every plan, so a key can always see why it was refused.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.
429 with error.code rate_limited and a Retry-After header: the number of whole seconds to wait.X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (seconds until there is room again).429 with error.code limit_reached, and nothing is charged for that request.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.
/v1/meYour 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"/v1/planYour 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"/v1/scanStart 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.
| Field | Where | What it is |
|---|---|---|
Idempotency-Key | Header | Send 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. |
url | Body, required | The website to audit, for example example.com. |
name | Body, optional | The business name, if you know it. Helps match its Google listing. |
town | Body, optional | The business town, if you know it. |
st | Body, optional | Two-letter US state code, for example NJ. |
profile | Body, optional | An audit template id from your workspace. |
business_type | Body, optional | One of auto, local, service_area, software, online. auto (the default) works it out from the website. |
webhook_url | Body, optional | Main key only. Saves the workspace's webhook address (https only). |
private | Body, optional | true 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"}'/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.
| Field | Where | What it is |
|---|---|---|
id | In the path | The 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"/v1/scansYour audits, most recently changed first, a page at a time (for polling).
| Field | Where | What it is |
|---|---|---|
status | In the address | Only audits with this status. |
site | In the address | Only audits of this website, for example example.com (subdomains included). |
limit | In the address | Page size. |
cursor | In the address | next_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"/v1/seatsYour 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.
| Field | Where | What it is |
|---|---|---|
status | In the address | Only keys in this state. |
limit | In the address | Page size. |
cursor | In the address | next_cursor from the previous page. |
Answers 200 OK when it works.
curl https://vouchfield.com/v1/seats \
-H "Authorization: Bearer YOUR_MAIN_API_KEY"/v1/seatsMake 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.
| Field | Where | What it is |
|---|---|---|
label | Body, optional | A 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"}'/v1/seats/{id}Switch off an extra API key (main key only).
| Field | Where | What it is |
|---|---|---|
id | In the path | The 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"/v1/integrations/slackPost finished-audit notes to a Slack incoming webhook (main key only; an empty url disconnects).
| Field | Where | What it is |
|---|---|---|
url | Body, required | Your 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"}'/v1/openapi.jsonThis 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/v1/docsThis API reference, as a web page. No key needed.
Answers 200 OK when it works.
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.
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.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).
202, with the header Idempotent-Replayed: true) and no second credit is used, even at your daily limit.url is refused with 409. Changed settings are not applied to the first audit.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.
| Status | error.code | What it means |
|---|---|---|
400 | bad_request | The request is not valid. |
401 | unauthorized | Missing or invalid API key. |
403 | forbidden | This API key cannot do that. |
404 | not_found | Not found. |
405 | method_not_allowed | That method is not allowed here. |
409 | conflict | The request conflicts with the current state. |
413 | too_large | The request is too large. |
415 | unsupported_media_type | Send the body as JSON with Content-Type: application/json. |
422 | invalid_request | Some fields are missing or not valid. |
429 | limit_reached | A limit was reached. |
500 | server_error | Something went wrong on our side. Try again later. |
502 | bad_gateway | A service we rely on did not answer. Try again later. |
503 | unavailable | The service is not available right now. |
403 | plan_limit | Your plan does not include this. The answer also has upgrade, the page to change plan. |
429 | rate_limited | Too many requests a minute. Wait the seconds in Retry-After. |
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.
https:// address.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.| Event | When it is sent |
|---|---|
scan.completed | An audit finished. The body is the same JSON as GET /v1/scan/{id} (status done). |
scan.failed | An 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.test | A 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."}| Header | What it means |
|---|---|
X-Audit-Event | What 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-Id | An 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-Timestamp | When the message was sent, in seconds since 1970. |
X-Audit-Signature | A signature made with your agency's signing secret. See the note on checking messages below. |
2xx status within 10 seconds, as soon as you have stored the message. Do the slow work afterwards.X-Audit-Id, so you can skip a message you already have.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.
X-Audit-Timestamp is more than 300 seconds (5 minutes) old, or more than 300 seconds in the future.timestamp + "." + raw_body.v1= in front.X-Audit-Signature using a constant-time comparison.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.
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)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.
The same endpoints as an OpenAPI 3 file, for Postman and other API tools: /v1/openapi.json. No key needed.
Questions? Ask us.