VisiPay

VisiPay help

The VisiPay API and webhooks

Your company's own software can read its VisiPay projects, pay applications, payment status and lien waivers, create draft supplier invoices, and be told when something changes. It is off until an Owner of your company makes an API key in Settings. Nothing about it can move money.

1. What a key can and cannot do

  • A key reads as the Owner who made it, on that person's Owner, GC and Sub seats for your company, and never more. It is served exactly what that person's screens show, with the same figures hidden: a GC's key never sees a Sub's own prices, and a Sub's key never sees the project's totals.
  • A project where that person holds no seat for your company is refused with the same answer whether it exists or not.
  • No call can fund a project, approve or release a payment, sign a lien waiver, submit a pay application or change a bank account. The one thing a key can create is a draft supplier invoice, and nothing is paid from it until a person assigns its parts on the screen.
  • Every call that reaches the key check with a working key is recorded on your company's audit record: the key, the address called and the answer's status, never what was sent or read. Only your company reads it.
  • A call refused before the key check is not recorded either: a 429 for too many requests from one address, a 413 for a body over 256 KB, and a 400 for a body that is not JSON. These answers carry no X-RateLimit headers.
  • A call made with an unknown, revoked or stopped key is refused and is not recorded on any company's audit record: until the key is known to work, VisiPay does not take the call as your company's.

2. Making a key

1. Open Settings and find API keys and webhooks. Only an active Owner of the company sees it, and only once VisiPay has approved the company.

2. Give the key a name, tick what the key may do, and choose Make key.

3. The key is shown once. Choose Copy and keep it somewhere safe, then choose Done. VisiPay keeps only a fingerprint of it, so it cannot be shown again; if it is lost, revoke it and make a new one.

A company may hold 10 keys that are not revoked. Each key is listed with its first characters, what it may do, who made it, when it was last used and its calls today.

3. What a key may do

  • projects:read: Read projects.
  • pay_applications:read: Read pay applications.
  • payments:read: Read payment status.
  • waivers:read: Read lien waivers.
  • supplier_invoices:write: Create draft supplier invoices.

4. Calling the API

Send each request to the address you sign in to VisiPay at, followed by /api/v1, with the key as a Bearer token. Answers are JSON. A list gives up to 50 rows a page, with page and hasMore; ask for the next with ?page=2.

A refusal has a status and a body of the form { "error": { "code", "message" } }: 401 for a missing, revoked or stopped key, 403 for a scope the key was not given, 404 for a project the key does not reach, 400 for a body that is not right, 409 for a supplier invoice already recorded, and 429 over a limit.

curl https://visipay.io/api/v1/projects \
  -H "Authorization: Bearer vpk_..."

The addresses

  • GET /api/v1The key itself: its company, its scopes and the version of the API.Any key.
  • GET /api/v1/projectsThe projects the key reaches, each with its state and the seat (owner, gc or sub) it is read under. 50 a page: ?page=2 for the next.Scope: projects:read (Read projects).
  • GET /api/v1/projects/{projectId}One project as its page shows it to the key's seat.Scope: projects:read (Read projects).
  • GET /api/v1/projects/{projectId}/pay-applicationsA project's pay applications and their status. 50 a page.Scope: pay_applications:read (Read pay applications).
  • GET /api/v1/projects/{projectId}/pay-applications/{payApplicationId}One pay application with its lines and each line's status.Scope: pay_applications:read (Read pay applications).
  • GET /api/v1/projects/{projectId}/paymentsThe payments on a project and the status of each, sent or settled, by party.Scope: payments:read (Read payment status).
  • GET /api/v1/projects/{projectId}/pay-applications/{payApplicationId}/release-conditionsWhat a pay application's payment waits on: each release condition, met or not.Scope: payments:read (Read payment status).
  • GET /api/v1/projects/{projectId}/lien-waiversA project's lien waivers and their status; ?payApplicationId= for one pay application's. 50 a page.Scope: waivers:read (Read lien waivers).
  • POST /api/v1/projects/{projectId}/supplier-invoicesCreate a draft supplier invoice for the key's company, every part unassigned. Body: supplierName, invoiceNumber, invoiceDate (YYYY-MM-DD), optional note, and parts, each with description, quantity, optional unit and amount in dollars and cents. Nothing is paid from it until a person assigns its parts on the Supplier invoices tab.Scope: supplier_invoices:write (Create draft supplier invoices).

5. Limits

Each key may make 60 calls a clock minute and 5,000 a calendar day, Central time. Over a limit the call is answered 429 with Retry-After, the seconds until it opens again. Every answer to a working key, a 429 included, carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Window, which names the window the first two count: minute on an answered call and over the minute limit, day over the day limit, where X-RateLimit-Limit is the day's 5,000 and X-RateLimit-Remaining is 0. A 401 for a missing, unknown, revoked or stopped key carries none of them, since no key's count was read.

6. Webhooks

A webhook is an https address of yours that VisiPay sends an event to when something changes. It belongs to one key and is sent only what that key can read: a project the key reaches, and only an event that key's seat sees on the project's audit record.

1. In Settings, under Webhooks, enter the address, choose the key it belongs to, tick the events to send, and choose Add webhook. An event the key cannot read cannot be ticked.

2. The webhook's signing secret is shown once. Choose Copy and keep it with your software, then choose Done.

3. Choose Send test to send a test event now; the page says what the address answered. One send by hand a webhook every 30 seconds, Send test and Send again together.

A company may have 10 webhooks turned on. Only events recorded after a webhook is added are sent to it. An address on a private network is refused.

7. The events

  • payment.settled: Payment settled. The key needs payments:read.
  • payment.held: Payment on hold. The key needs payments:read.
  • pay_application.status_changed: Pay application status. The key needs pay_applications:read.
  • waiver.signed: Lien waiver signed. The key needs waivers:read.

8. What an event looks like

Each event is a POST with a JSON body naming its id, its type, when it happened and the ids of what it names. It never carries an amount, a name or a person: read those through the API, where your seat's rules apply. The id is the same on every retry, so your software can drop a repeat. A payment is sent as settled once, however many records name it.

{
  "id": "9b2f...",
  "type": "payment.settled",
  "apiVersion": "v1",
  "createdAt": "2026-10-25T16:00:00.000Z",
  "data": { "projectId": "...", "paymentId": "...", "payApplicationId": "...", "kind": "pay_application" }
}

9. Checking the signature

Every request carries VisiPay-Webhook-Id, VisiPay-Webhook-Timestamp and VisiPay-Signature, which reads t=<timestamp>,v1=<signature>. The signature is HMAC SHA-256, in hex, of the timestamp, a full stop and the raw body, keyed with the webhook's signing secret.

1. Read the raw body as it arrived, before any JSON parsing. 2. Work out the HMAC of the timestamp, a full stop and that body. 3. Compare it with v1 in constant time. 4. Refuse a timestamp more than 5 minutes from your clock. 5. Answer with any 2xx status once you have stored the event.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, header, rawBody) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(t + "." + rawBody).digest();
  const given = Buffer.from(parts.v1 ?? "", "hex");
  return given.length === expected.length && timingSafeEqual(expected, given);
}

10. Retries and the delivery log

A delivery not answered with a 2xx status within 10 seconds in all, however slowly your address sends, is tried again after 1, 5 and 30 minutes, then 2, 6, 12 and 24 hours: 8 tries in all over about two days. Then it is marked failed, and kept. A redirect is not followed.

Choose Delivery log beside a webhook to see each event sent to it, newest first, 50 at a time: its type, when, its id, and its last try with the status the address answered or why none came. Choose Send again beside a delivery to send it once more now, with the same id: it is a new entry in the log, tried once, and the delivery it copies keeps its own schedule. Send test and Send again share one cooldown of 30 seconds a webhook.

11. Revoking a key, turning a webhook off

Choose Revoke beside a key, then Revoke again to confirm. The key stops at once: its next call is refused and nothing more is sent to the webhooks under it. Any Owner of the company may revoke any of its keys. A revoked key is kept on the list; choose Show revoked keys to see it.

Choose Turn off beside a webhook, then Turn off again to confirm. Nothing more is sent to it, and its delivery log stays.

Choose Rotate secret beside a webhook, then Rotate secret again to confirm, to replace its signing secret. The new secret is shown once; from that moment every request is signed with it and the old secret no longer checks, so put the new one in your software straight away.

If a webhook says its signing secret can no longer be read, VisiPay cannot sign for it and sends it nothing; the delivery log says so beside each try. Choose Rotate secret, or turn it off and add it again.

A key also stops, without being revoked, while the person who made it is no longer an active Owner of the company or cannot sign in, or while the company is not active. Settings says so beside the key; make a new key to carry on.

Questions

Email support@visipay.io and a person on our team will answer.