Agent setup guide
Complete the approved website setup without the dashboard.
Request owner authorization, preserve one exchange secret, use narrow scopes, add the read-only deploy key, prove readiness, and send one safe test report.
Before you start
Use an owner email and website authority that the owner gave you through a trusted channel.
1. Start owner enrollment
Choose only the scopes required for this setup. Enrollment does not permit credential management.
curl --fail-with-body https://issueprobe.com/v1/agent-enrollments \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: setup-owner-example-001' \ -H 'X-IssueProbe-Client: setup-agent-example' \ --data '{ "ownerEmail": "owner@example.com", "accountName": "Example account", "agentName": "website-setup-agent", "scopes": [ "websites:write", "websites:read", "reports:read", "reports:write", "audit:read", "billing:checkout" ] }'The neutral response contains an enrollment identifier, one exchange secret, and a seven-day expiry.
2. Preserve the exchange secret
Save the exchange secret in the caller's approved secret location before the process ends. Do not put it in a URL, email, log, commit, or issue.
An exact retry with the same client identifier, idempotency key, and request can recover the same unused secret while the enrollment remains pending or approved. A changed retry fails.
3. Wait for owner approval
IssueProbe emails the owner one authorization link. A link GET cannot approve the request. The owner reviews the account name, agent name, and exact scopes, then uses an explicit POST to approve or reject.
Do not send repeated enrollment requests while you wait. Ask the owner through the existing trusted channel that started the setup.
4. Exchange once for a credential
After approval, exchange the preserved secret once.
curl --fail-with-body https://issueprobe.com/v1/agent-enrollments/ENROLLMENT_ID/exchange \ -H 'Content-Type: application/json' \ --data '{"exchangeSecret":"COPY_THE_EXCHANGE_SECRET"}'The response shows one scoped API credential once. Store it safely. A repeated exchange fails without a second credential. The issued credential expires after 90 days.
5. Register the website, origin, and SSH repository
Use the credential as a bearer value. Register one root origin and one SSH repository location.
curl --fail-with-body https://issueprobe.com/v1/websites \ -H 'Authorization: Bearer YOUR_API_CREDENTIAL' \ -H 'Content-Type: application/json' \ --data '{ "name": "Example Site", "primaryOrigin": "https://www.example.com", "position": "right", "repository": { "sshUrl": "git@example.com:owner/site.git", "defaultBranch": "main", "hostKeyFingerprint": "SHA256:VERIFIED_HOST_FINGERPRINT" } }'Omit
deployedRevisionto use branch-head mode. Supply one hexadecimal revision only when the owner requires pinned mode.The response contains the public widget key and the public deploy key. It never contains a private repository key.
6. Add and verify the exact origin
Read the website detail. Each pending origin includes one exact HTTPS verification URL and one 64-character public text value.
curl --fail-with-body https://issueprobe.com/v1/websites/WEBSITE_ID \ -H 'Authorization: Bearer YOUR_API_CREDENTIAL' # Publish the returned verification.content as the complete text at verification.url. # The file can end with one newline. Do not use a redirect. curl --fail-with-body https://issueprobe.com/v1/websites/WEBSITE_ID/origins/ORIGIN_ID/verify \ -H 'Authorization: Bearer YOUR_API_CREDENTIAL' \ -H 'Content-Type: application/json' \ --data '{"method":"well_known"}'The verification text is public placement content, not a credential. A widget key or caller-supplied Origin header cannot replace it.
Paths, credentials, query values, fragments, HTTP origins, redirects, and different file content fail verification.
7. Add the public deploy key
Add the returned public key to the registered repository with read-only access. Use one key for this website only. Do not grant write authority.
Keep the verified SHA-256 host fingerprint in the website setup. Remove an old public key after a rotation.
8. Check repository and website readiness
curl --fail-with-body https://issueprobe.com/v1/websites/WEBSITE_ID/repository/check \ -X POST \ -H 'Authorization: Bearer YOUR_API_CREDENTIAL' \ -H 'Content-Type: application/json' \ --data '{}' curl --fail-with-body https://issueprobe.com/v1/websites/WEBSITE_ID/readiness \ -H 'Authorization: Bearer YOUR_API_CREDENTIAL'Continue only when the origin, repository, and widget checks report ready.
In branch-head mode, run the repository check after each deployment. Each successful check records the current remote branch head. Pinned mode keeps the owner-supplied revision.
9. Install the widget
Set the required evidence categories through the fixed schema. A category can be disabled, but no setting can enable browser-state values.
curl --fail-with-body https://issueprobe.com/v1/websites/WEBSITE_ID/evidence-settings \ -X PUT \ -H 'Authorization: Bearer YOUR_API_CREDENTIAL' \ -H 'Content-Type: application/json' \ --data '{"screenshot":true,"console":true,"network":true,"browserStateMetadata":true}' curl --fail-with-body https://issueprobe.com/v1/websites/WEBSITE_ID/install \ -H 'Authorization: Bearer YOUR_API_CREDENTIAL'Place the returned script on the verified website. Its public widget key grants no account or repository access.
The response carries two addresses. The
scriptfield uses the rolling address, which updates itself. Never add anintegrityattribute to it: the bundle changes at each IssueProbe release, the hash stops matching, and the browser then refuses the script. Usepinned.scriptwhen a policy requires Subresource Integrity. That address is content addressed and immutable, and it needs a manual update at each release.Two owner appearance settings travel with the website:
launcherStyleisfullorcompact, andaccentColoris a six-digit hexadecimal colour or null. The compact style keeps the full accessible name, and the label colour follows the accent luminance automatically.The widget host uses
z-index: 2147483000, so the launcher paints above normal page content. A top corner can cover a fixed page header. SetoffsetXoroffsetYthroughPATCH /v1/websites/{websiteId}, from 0 to 160 pixels, or choose a bottom corner.The script also defines
window.IssueProbewithhide(),show(),open(),close(), andversion. Callhide()while your own modal is open andshow()when it closes. Callopen()from your own report link.myModal.addEventListener('open', () => window.IssueProbe.hide()); myModal.addEventListener('close', () => window.IssueProbe.show()); document.querySelector('#report').addEventListener('click', () => window.IssueProbe.open());hide()andshow()change page visibility alone. They write no browser storage and never change the corner or the collapsed state that the visitor chose, so every page load starts with a visible widget.A custom loader that calls
window.IssueProbeWidget.mount()with its ownconfigurationobject owns every field, so a new owner setting never reaches that page. Pass theGET /v1/public/widget-configresponse through unchanged instead of building the object field by field. The plain script tag does this for you.A host stylesheet cannot hide or restyle the widget. The widget writes
all: initial !importanton its own element, and an inline declaration with!importantbeats an author rule that also carries!important. A rule such as[data-issueprobe-widget]{visibility:hidden!important}therefore has no effect. That armor keeps hostile or careless page CSS from breaking the report channel. Usehide()andshow()instead.The snippet loads the script with
async, sowindow.IssueProbeappears only when the script runs. Call the API from a user action or after theloadevent. A page that must control the widget earlier installs a stub with aqarray before the script tag. The widget replays that queue in order and accepts only the four command names.The widget adopts its styles as CSSOM, so its shadow root holds no style element and no style attribute. A
style-srcdirective needs no hash for the widget and no'unsafe-hashes'. An existing pinned hash keeps working and is no longer needed, except on a browser that cannot construct a stylesheet and takes the style-element fallback.10. Submit a safe test report
Create a JSON file named
test-report.jsonwith no private values.{ "websiteKey": "PUBLIC_WIDGET_KEY", "type": "problem", "description": "The profile save button does not finish.", "page": { "url": "https://www.example.com/profile", "title": "Profile", "locale": "en-US", "timeZone": "UTC", "viewport": {"width": 1440, "height": 900} }, "console": [], "network": [], "browserState": {"cookies": [], "formFields": [], "localStorage": []}, "consent": {"screenshot": false, "evidence": true} }curl --fail-with-body 'https://issueprobe.com/v1/public/reports?websiteKey=PUBLIC_WIDGET_KEY' \ -H 'Origin: https://www.example.com' \ -H 'Idempotency-Key: safe-test-report-001' \ -H 'X-IssueProbe-Client: setup-agent-example' \ -F 'payload=<test-report.json'Cookie, form, and local-storage entries can contain only a name, presence state, and size. Request evidence contains no header, body, credential, query value, or fragment.
11. Read reports and results
curl --fail-with-body 'https://issueprobe.com/v1/reports?limit=25' \ -H 'Authorization: Bearer YOUR_API_CREDENTIAL' curl --fail-with-body https://issueprobe.com/v1/reports/REPORT_ID \ -H 'Authorization: Bearer YOUR_API_CREDENTIAL'Use
reports:readto read customer-safe reports. Usereports:writefor a draft, review, archive, or new investigation. A saved draft is not sent to the visitor.12. Start hosted Checkout when authorized
The credential needs
billing:checkout. Use a new idempotency key for one logical upgrade attempt.curl --fail-with-body https://issueprobe.com/v1/billing/checkout \ -X POST \ -H 'Authorization: Bearer YOUR_API_CREDENTIAL' \ -H 'Idempotency-Key: owner-pro-upgrade-001' \ -H 'Content-Type: application/json' \ --data '{}'Give the hosted URL to the owner. Do not collect card data. A Checkout return does not grant Pro by itself.
13. Handle expiry and revocation safely
The owner authorization and exchange expire after seven days. The issued credential expires after 90 days. Stop on a
401,403, or expiry error and ask the owner for a new approved enrollment.Only a human owner can revoke a credential through
DELETE /v1/api-credentials/{credentialId}. The agent cannot create or revoke another agent credential. Remove the stored credential after revocation.
Optional: report notifications
An owner can ask for one signed HTTPS POST when a visitor sends a report. The credential needs websites:write to set the webhook and websites:read to read it. Use a public HTTPS address on port 443. A query string is allowed.
curl --fail-with-body https://issueprobe.com/v1/websites/WEBSITE_ID/webhook \
-X PUT \
-H 'Authorization: Bearer YOUR_API_CREDENTIAL' \
-H 'Content-Type: application/json' \
--data '{"url":"https://hooks.example.com/issueprobe","enabled":true}'
curl --fail-with-body https://issueprobe.com/v1/websites/WEBSITE_ID/webhook/test \
-X POST \
-H 'Authorization: Bearer YOUR_API_CREDENTIAL' \
-H 'Content-Type: application/json' \
--data '{}'The first PUT returns 201 with signingSecret once. Store it in the owner's secret store immediately. A later PUT returns 200 without a secret. Use the signing secret rotation operation for a new secret. The old secret stops at once.
The report.received body contains the website, the report identifier, reference, type, state, receipt time, and two links. It never contains the report text, the contact email, browser evidence, a screenshot, or a result. Read the report through GET /v1/reports/{reportId} with reports:read.
Each request uses the Standard Webhooks v1 scheme. Verify the raw body with the webhook-id, webhook-timestamp, and webhook-signature headers. The official Standard Webhooks libraries also verify it. This example uses only node:crypto:
import { createHmac, timingSafeEqual } from "node:crypto";
const processed = new Set(); // Use a shared store with an expiry in production.
// rawBody is the exact request body text. Verify it before any JSON parse.
export function verifyIssueProbeEvent(rawBody, headers, signingSecret, nowMs = Date.now()) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
const signatures = headers["webhook-signature"];
if (!id || !timestamp || !signatures) throw new Error("A webhook header is missing.");
if (!/^[0-9]+$/.test(timestamp) || Math.abs(nowMs / 1000 - Number(timestamp)) > 300) {
throw new Error("The webhook timestamp is outside five minutes.");
}
const key = Buffer.from(signingSecret.slice("whsec_".length), "base64");
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest();
const valid = signatures.split(" ").some((entry) => {
const [version, value] = entry.split(",");
if (version !== "v1" || !value) return false;
const actual = Buffer.from(value, "base64");
return actual.length === expected.length && timingSafeEqual(actual, expected);
});
if (!valid) throw new Error("The webhook signature is invalid.");
if (processed.has(id)) return null; // The same event arrived again. Do not process it twice.
processed.add(id);
return JSON.parse(rawBody);
}Return any 2xx status within 15 seconds. IssueProbe does not follow a redirect. A failed delivery has six retries after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, and 12 hours. Read the delivery history with GET /v1/websites/{websiteId}/webhook.
Scope reference
| Scope | Use |
|---|---|
websites:write | Register and change website setup. |
websites:read | Read websites, installation data, and readiness. |
reports:read | Read reports and customer-safe results. |
reports:write | Save drafts, record review, archive, and request another investigation. |
audit:read | Read customer-safe account activity. |
billing:checkout | Start hosted Checkout for an owner. |
credentials:write is not an enrollment scope. Only a human owner can create or revoke API credentials.
Safe error handling
- 400: Correct the documented request shape. Do not add undocumented fields.
- 401: Stop. The credential, claim, or exchange value is invalid or expired.
- 403: Stop. The account, scope, exact origin, or owner authority does not permit the action.
- 409: Read the stable error code. Do not create a changed retry with an old idempotency key.
- 410: The enrollment or artifact has expired. Ask the owner for the next authorized action.
- 429: Respect the public limit. Do not change client identifiers to bypass it.
Keep the response requestId for owner support. Do not log credentials, exchange values, owner authorization values, or sensitive report evidence.
Use the current machine-readable contract.
The rendered reference and OpenAPI JSON define the same public operations.