Datacove API Developer guides
MSP · Client integration

MSP seat client guide

For teams building the Chrome, Edge, Firefox or Safari extension, the Outlook add-in, or the mobile app that an MSP's end users run. It covers activating a seat on a device, keeping its session alive, and running workflows as that seat.

Base URLhttps://<api-host>/api
CredentialsLicense token · Session token
User roleSEAT_USER
BillingBilled to the MSP · never per run

01How seats work

An MSP (managed service provider) creates organizations for its clients and invites their staff as seats. A seat holder never creates a password. The invite email gives them two things: their Organization ID (for example ACME_CORP) and one activation code per product they were given, such as DNET-A7K2-9FQ4-MMX4-P3ZR. Your client turns those into two credentials, each accepted in a different place.

Client Org ID + code + installId activate License token RS256 · 7 days one per device validate · daily license/session Session tokens access 30 min refresh 7 days renew · auth/v1/refresh Workflows execute
The license token proves this device holds a valid seat. The session token is what workflow calls accept. Neither is accepted where the other belongs.
CredentialLifetimeComes fromSend it to
installIdLife of the installYour client: a UUID v4 generated on first runactivate, validate
License token7 days (+7-day grace)license/activate, license/validatelicense/validate, license/session, license/deactivate
Access token30 minuteslicense/session, auth/v1/refreshWorkflow execution, reports, realtime ticket
Refresh token7 days, rotated on useSame as the access tokenauth/v1/refresh

What your client stores

The installId (permanently), the licenseToken with its tokenExpiresAt, and the current access and refresh tokens. Use the platform's secure storage. You don't need the activation code again after a successful activation.

No payment flow, ever

Seats are billed to the MSP for each activated device. A seat is never asked for credits, a checkout, or a subscription, so there's no purchase or paywall screen to build.

02Activating a device

First runActivatePOST msp/v1/license/activate
Every 24 hValidatePOST msp/v1/license/validate
Moving deviceDeactivatePOST msp/v1/license/deactivate

Activate

POST/msp/v1/license/activate No authThe code is the credential

Each activation code works on exactly one device. Calling again with the same installId returns a fresh license token without using another seat, so the same call handles reinstalls and recovery.

FieldTypeNotes
organizationIdrequiredstringThe Organization ID from the invite email: 2–64 letters, digits, _ or -. Not case-sensitive.
coderequiredstringDNET-XXXX-XXXX-XXXX-XXXX, uppercase letters A–Z and digits 2–9.
installIdrequiredUUID v4Generated once on first run and reused for the life of the install.
platformconditional"ios_app" | "android_app"Required for a mobile-app code. Ignored for extension and Outlook codes, whose platform comes from the code itself.
extensionVersionoptionalstringYour client version, for example 1.0.3.
deviceFingerprintoptionalstringA hash of coarse device signals, used only for fraud signals.
JavaScript
const res = await fetch(`${BASE_URL}/msp/v1/license/activate`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    organizationId: "ACME_CORP",
    code: "DNET-A7K2-9FQ4-MMX4-P3ZR",
    installId,                 // crypto.randomUUID(), stored on first run
    extensionVersion: "1.0.3",
  }),
});
const license = await res.json();
Response · 200
{
  "status": "activated",
  "licenseToken": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
  "tokenExpiresAt": "2026-10-09T09:30:00.000Z",
  "plan": "msp",
  "licenseExpiresAt": null
}
StatusMeaning
200Activated, or re-activated on the same installId. licenseExpiresAt: null means the seat has no end date; it stops only when revoked.
401 invalid_codeWrong Organization ID, a code from another organization, or a revoked or expired code. All look the same on purpose.
403 seat_limit_reachedThis code is already active on another device. Deactivate it there first.
400A malformed field, or a mobile-app code sent without platform.
429More than 30 attempts a minute, or 300 an hour, from one IP address. Wait and try again; show a countdown rather than retrying in a loop.
503 license_signing_unavailableThe server can't issue license tokens right now. Nothing was saved and no seat was used. Retry later with the same installId.

Daily validate

POST/msp/v1/license/validate License tokenHeartbeat

Call it about every 24 hours and on every start. It confirms the seat is still valid and returns a new 7-day license token; replace the stored one each time. A token that expired up to 7 days ago is still accepted, so a device that was offline for a week recovers without the user doing anything.

JavaScript
const res = await fetch(`${BASE_URL}/msp/v1/license/validate`, {
  method: "POST",
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${licenseToken}` },
  body: JSON.stringify({ installId, extensionVersion: "1.0.4" }),
});
// Same shape as activate, with "status": "validated". Store the new licenseToken.
StatusMeaning
200Seat is valid. Store the returned token.
401 license_invalidThe seat or this device was revoked, or the organization or MSP is suspended. Lock protected features.
401 license_expiredExpired more than 7 days ago. Show the activation screen; the same code and installId reactivate without using another seat.
503 license_signing_unavailableTemporary server problem. Keep the current token and try again later; don't lock the user out for this.

Deactivate and verify locally

POST/msp/v1/license/deactivateLicense token

No body. Frees this device's slot so the code can activate somewhere else. Needs an unexpired token, so call validate first if you're unsure. Safe to repeat. Returns { "status": "deactivated" }.

GET/msp/v1/license/.well-known/jwks.jsonNo auth

The RS256 public keys that sign license tokens. Use them to check a token's signature and expiry offline. Cache for 24 hours, and pick the key whose kid matches the token header.

03Getting a session

The license token is never accepted for workflow calls. Exchange it once for a normal session, then keep that session alive the same way every other Datacove client does.

POST/msp/v1/license/session License tokenExchange for a session

No body. Returns a token pair for the seat holder, carrying the workflows the MSP's plan includes.

Response · 200
{
  "accessToken": "eyJhbGciOiJIUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJIUzI1NiIs...",
  "expiresIn": 1800
}

401 license_invalid under the same conditions as validate.

Exchange once, then refresh

Every call to license/session creates a new session, and one person can hold at most 5 sessions across all their devices. A sixth signs out their oldest device. So call it once after activation, or after the session is lost, and renew with POST /auth/v1/refresh about a minute before expiresIn runs out. Refresh re-reads the MSP's plan each time, so plan changes reach the device without a new exchange.

JavaScript · keeping a seat session
async function getAccessToken() {
  const s = await storage.get("session");
  if (s && Date.now() < s.expiresAt - 60_000) return s.accessToken;
  if (s) {
    const res = await fetch(`${BASE_URL}/auth/v1/refresh`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ refreshToken: s.refreshToken }),
    });
    if (res.ok) return save(await res.json());
  }
  // No session yet, or refresh was rejected: exchange the license again.
  const res = await fetch(`${BASE_URL}/msp/v1/license/session`, {
    method: "POST",
    headers: { Authorization: `Bearer ${await storage.get("licenseToken")}` },
  });
  if (!res.ok) throw new LicenseError((await res.json()).error?.message); // license_invalid
  return save(await res.json());
}

async function save({ accessToken, refreshToken, expiresIn }) {
  await storage.set("session", { accessToken, refreshToken, expiresAt: Date.now() + expiresIn * 1000 });
  return accessToken;
}

Run renewals one at a time: if several calls need a new token at once, they should share one refresh. A refresh token that has already been replaced and is used again ends the session as a precaution. The Android guide's session section lists the 401 codes you may see (SESSION_EVICTED, SESSION_REVOKED, TOKEN_REUSED, REFRESH_INVALID).

04Client state rules

WhenDo this
Client starts with a stored license tokenCall validate and store the new license token. Use the stored session if there is one; otherwise call license/session.
Access token within a minute of expiryCall auth/v1/refresh.
A workflow call returns 401Refresh once and retry. If refresh fails, call license/session. If that fails too, continue below.
A license call returns license_invalidThe seat or device was revoked, or the organization was suspended. Stop protected features and show "Contact your administrator".
A license call returns license_expiredShow the activation screen. The same code and installId work again without using another seat.
User signs out or uninstallsCall license/deactivate first so the seat can move to another device.

All devices one person activates share the same seat user, so runs from their browser extension and their phone appear together. Your client never sends its own identity; the server works it out from the activation.

05Running workflows

Which workflows a seat can run

The MSP's plan with Datacove decides this, for every organization under that MSP. A workflow outside the plan behaves as if it doesn't exist: 404, not 403. Seat sessions can't read workflow definitions, so ship the ids and input keys you need with your client. The reference below lists them.

POST/workflows/v1/execute Access tokenJSON or multipart
FieldTypeNotes
idrequiredstringThe workflow id, for example wf-sec-001.
inputrequiredobjectOne property per input key.
countryoptionalstringISO country code, for example "in".
JavaScript · text input
const res = await fetch(`${BASE_URL}/workflows/v1/execute`, {
  method: "POST",
  headers: { "Content-Type": "application/json", Authorization: `Bearer ${await getAccessToken()}` },
  body: JSON.stringify({ id: "wf-sec-001", input: { url: "https://login-verify-account.example" } }),
});
const result = await res.json(); // instant workflow: this is the analysis

Sending files

Attach files to the execute call itself. Seat clients can't use /media/v1/upload, which needs an app API key. Send id as a plain text field, every non-file input in one text field named input as a JSON string ({} if there are none), and each file as a part named after its input key. Don't set Content-Type yourself; let the HTTP client add the multipart boundary.

JavaScript · multipart
const form = new FormData();
form.append("id", "wf-sec-008");
form.append("input", JSON.stringify({}));              // non-file inputs, none here
form.append("document", fileBlob, "contract.pdf");   // part name = the input's key

const res = await fetch(`${BASE_URL}/workflows/v1/execute`, {
  method: "POST",
  headers: { Authorization: `Bearer ${await getAccessToken()}` },
  body: form,
});
const { eventId } = await res.json();                  // report workflow: see the next section

Instant result

The 201 body is the answer. Nothing else to call. If the body contains "headers": { "success": false }, the workflow ran but couldn't produce an answer. Treat that as "no result", not as a verdict.

Report

The 201 confirms the run started: { "success": true, "eventId": "…", "message": "…" }. Keep the eventId.

StatusMeaning
201The result, or a report's eventId.
400Missing id, or an input failing validation: a missing required key, wrong file type, or file too large. Also returned when the workflow itself rejects the input: error.message is The workflow could not process this input, with error.detail or error.fields ([{ field, issue }]) when available. Retrying the same input fails again.
401Access token expired or the session ended. Renew and retry once.
404Not in the MSP's plan, not enabled, or not a real id.
409Only for workflows that cost credits, and for report workflows: this seat just submitted the same workflow with identical input and it's still running. Wait for it. Identical requests to a free instant workflow both run.
502The workflow service failed or couldn't be reached. Show “couldn't check right now”, not a verdict, and retry with backoff.
Cache results on the device

Every request runs the workflow again, so don't send the same check twice in a short time. For URL checks, keep each result for a while (10 to 30 minutes works well) and reuse it for repeat visits, other tabs and reloads. Also share one in-flight request when the same URL is checked again before the first answer arrives.

06Report results

GET /workflows/reports/v1/:eventIdMeaning
200 { "status": "GENERATING" }Still running.
200 (report body)Finished.
404The run failed, or there's no such report. These look the same on purpose.

Report history, sharing and deletion aren't available to seat sessions. Keep your own list of eventIds if your client shows past results.

07Workflow reference

The security workflows MSP plans draw from, as currently defined. Your MSP's plan decides which ones a seat can run. Input keys and file limits can change; re-check this table when you update your client.

WorkflowResultInput keyAcceptsMax size
wf-sec-001Instanturl (text)A full URL including https://—
wf-sec-002Reportdocument (file).pdf .doc .docx10 MB
wf-sec-003Reportdocument (file).pdf10 MB
wf-sec-004Reportdocument (file).jpg .jpeg .png .webp5 MB
wf-sec-005Reportdocument (file).mp4 .webm .mov .avi100 MB
wf-sec-006Reportdocument (file).pdf5 MB
wf-sec-007Reportimage (file).jpg .jpeg .png .webp5 MB
wf-sec-008Reportdocument (file).pdf .doc .docx10 MB

08Errors

Every error uses the same envelope. On license routes, error.message is a stable code such as invalid_code; branch on it.

Error envelope
{
  "statusCode": 401,
  "timestamp": "2026-10-02T09:30:00.000Z",
  "path": "/api/msp/v1/license/activate",
  "error": { "statusCode": 401, "error": "Unauthorized", "message": "invalid_code" }
}
Statuserror.messageWhereWhat to do
401invalid_codeactivateAsk the user to re-check the Organization ID and code.
403seat_limit_reachedactivateThe code is in use on another device. Deactivate it there, or ask the administrator.
401license_invalidvalidate, sessionRevoked or suspended. Lock protected features.
401license_expiredvalidateShow the activation screen.
409seat_user_conflictactivateThe seat holder's account couldn't be set up. Ask the administrator to contact support.
503license_signing_unavailableactivate, validateTemporary. Retry later; on validate keep the current token.
401(has error.code)refresh, workflowsThe session ended. Exchange the license again with license/session.
429(varies)activateToo many attempts from this IP address. Wait before trying again.
400The workflow could not process this inputworkflowsThe workflow rejected this input. Show error.detail or error.fields; don't retry unchanged.
502Workflow service unavailable. Try again later.workflowsTemporary. Retry with backoff; treat the check as not done.