MyCaseViewer

API & webhooks

Connect MyCaseViewer to your case management system, your CRM, or anything else that can make an HTTP request.

Getting started

The API is included with the Corporate and Enterprise plans. Create a key in your dashboard under API & integrations. A key is shown once, when you create it — copy it then, because only a hash of it is stored and it cannot be shown again.

A key is either read (list cases and files) or read & write (also create cases, upload files, send links and manage webhooks). Give each integration its own key, so you can revoke one without breaking the others. A revoked key stops working on its next request.

Every request goes to https://www.mycaseviewer.com/api/v1 and carries the key as a bearer token:

curl https://www.mycaseviewer.com/api/v1/me \
  -H "Authorization: Bearer mcv_live_…"

Every response is JSON in one envelope. Success is an object under data; failure is a code and a message under error, with a matching HTTP status.

{ "data": { "account": { "id": "…", "email": "you@firm.com", "plan": { "code": "corporate", "name": "Corporate" } },
            "key": { "id": "…", "scope": "write" } } }

{ "error": { "code": "insufficient_scope",
             "message": "This key is read-only. Create a write key for this request." } }

Limits and errors

Each key may make 120 requests per minute. Past that the API answers 429 with a Retry-After header, in seconds. Sending case links shares your account's limit of 20 delivery emails an hour with the dashboard.

StatusCodeMeaning
400invalid_requestThe body or a parameter is missing or malformed.
401unauthorizedThe key is missing, malformed, unknown or revoked.
403plan_requiredThe account's plan no longer includes API access.
403trial_expiredThe account's trial has ended.
403insufficient_scopeA read key was used for a write request.
403account_disabledThe account can't use the API.
403case_limit / webhook_limitThe plan's case limit, or the 10-endpoint limit, is reached.
404not_foundNo such case, upload or webhook on this account.
409case_lockedThe case is sealed or under legal hold and can't be changed.
413quota_exceededThe file is over the plan's per-file limit, or storage is full.
422rejected / unsupported_typeThe request was understood and refused; the message says why.
429rate_limitedToo many requests. Wait for Retry-After.

The API runs the same code as the dashboard, so every rule is the same: plan limits, storage quota, a sealed delivery refusing changes. It deliberately cannot delete a case or a file, seal or unseal a delivery, or place or release a legal hold — those stay in the dashboard.

Cases

RequestScopeWhat it does
GET /mereadThe account and plan this key belongs to.
GET /casesreadYour cases, newest first. q searches title and recipient name; limit (default 50, max 200) and offset page through them.
GET /cases/{id}readOne case with its files, link, expiry and view counts.
POST /caseswriteCreate a case. Send title, and optionally recipientName.
POST /cases/{id}/sendwriteEmail the case link. Send recipients (an address, or a list of up to 50) and optionally message.

Creating a case returns its password, once. Every case is password-protected and the password is generated for you. It is in the response to POST /cases and in no other response, and it is never included in the email that Send sends — give it to your client separately.

curl -X POST https://www.mycaseviewer.com/api/v1/cases \
  -H "Authorization: Bearer mcv_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "title": "Whitfield v. Coastal Mutual", "recipientName": "Dana Whitfield" }'

{ "data": {
    "case": { "id": "c26e77d0-…", "title": "Whitfield v. Coastal Mutual",
              "recipientName": "Dana Whitfield",
              "shareUrl": "https://www.mycaseviewer.com/p/…",
              "passwordProtected": true, "files": [] },
    "password": "7GK4-Q2XZ-M8PR-3HNW" } }

A case in a response never includes its password, storage locations, or anything about a recipient beyond view and download counts.

Uploading files

File bytes never pass through our servers. You ask for an upload, send the bytes straight to storage at the address you are given, then tell us you have finished.

  1. POST /cases/{id}/uploads with filename, mimeType and sizeBytes (and optionally title). The reply has a fileId and a mode.
  2. mode "single" (files up to 64 MB): PUT the whole file to url with the headers given.
  3. mode "multipart" (larger files): cut the file into partSize pieces, PUT each piece to its numbered URL in parts, and keep the ETag header each PUT returns. If you need URLs for more parts than you were given, POST fromPartNumber to /uploads/{fileId}/parts.
  4. POST /uploads/{fileId}/complete. For a multipart upload, send parts: a list of partNumber and etag. The file is checked — its real size, and that its contents match its type — and then goes live in the case.

To abandon an upload, DELETE /uploads/{fileId}; it releases the storage it had reserved. An upload that is never completed is cleaned up automatically. Upload URLs expire, so start the upload when you are ready to send.

curl -X POST https://www.mycaseviewer.com/api/v1/cases/c26e77d0-…/uploads \
  -H "Authorization: Bearer mcv_live_…" -H "Content-Type: application/json" \
  -d '{ "filename": "report.pdf", "mimeType": "application/pdf", "sizeBytes": 482113 }'

{ "data": { "fileId": "4f68e00d-…", "mode": "single",
            "url": "https://…", "headers": { "Content-Type": "application/pdf" } } }

curl -X PUT "https://…" -H "Content-Type: application/pdf" --data-binary @report.pdf

curl -X POST https://www.mycaseviewer.com/api/v1/uploads/4f68e00d-…/complete \
  -H "Authorization: Bearer mcv_live_…"

If the case will be sealed, send a fingerprint. A sealed delivery records the SHA-256 of every file, and that hash is taken where the file is sent from — by the dashboard's uploader, or by you. Add fingerprint to the complete request: sha256 (the whole file, lowercase hex), partSize (the piece size you hashed in, in bytes) and parts (the SHA-256 of each piece, in order; one entry for a single-PUT file). A file completed without one uploads normally, but a case containing it cannot be sealed.

Webhooks

A webhook tells your system the moment something happens. Add an endpoint in the dashboard under API & integrations, or with the API. Its signing secret is shown once, when it is created.

RequestScopeWhat it does
GET /webhooksreadYour endpoints. A read key sees each endpoint's domain only, not its full address.
POST /webhookswriteSubscribe. Send url and events (a list). Returns the endpoint and its secret.
DELETE /webhooks/{id}writeUnsubscribe.
GET /events?type=…readA few sample events of one type, in the shape a webhook delivers — for building an integration, not an event log.
EventSent whendata
case.openedA recipient opens a case for the first time. Once per case.case, openedAt, country
file.downloadedA recipient downloads a file.case, file, downloadedAt, country
case.expiringThree days, and again one day, before a case's files expire.case, expiresAt, daysLeft
upload.completedA file finishes uploading to a case.case, file, uploadedBy, completedAt

case is the id, title and recipient name; file is the id, title, original filename, kind and size. An event never contains a recipient's IP address, the contents of a file, a link to a file, or a case password.

POST https://your-endpoint.example/hooks/mycaseviewer
Content-Type: application/json
MCV-Event-Id: evt_f22dabefe27580532f22ce787b6b4f3a
MCV-Event-Type: case.opened
MCV-Signature: t=1791051424,v1=ac64b2f6…

{ "id": "evt_f22dabefe27580532f22ce787b6b4f3a",
  "type": "case.opened",
  "createdAt": "2026-10-03T18:17:03.982Z",
  "data": {
    "case": { "id": "c26e77d0-…", "title": "Whitfield v. Coastal Mutual", "recipientName": "Dana Whitfield" },
    "openedAt": "2026-10-03T18:17:03.324Z",
    "country": "US" } }

Checking the signature

Every request carries MCV-Signature: t=<timestamp>,v1=<signature>. The signature is the HMAC-SHA256, in hex, of the timestamp, a full stop, and the raw request body, using your endpoint's secret. Recompute it and compare; reject the request if it does not match, or if the timestamp is more than five minutes old.

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

// rawBody must be the exact bytes received, before any JSON parsing.
export function isFromMyCaseViewer(rawBody, header, secret) {
  const match = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header ?? "");
  if (!match) return false;
  const [, timestamp, signature] = match;
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

Delivery

  • Answer with any 2xx status within 10 seconds to accept an event. Anything else, including a redirect, counts as a failure.
  • A failed delivery is retried up to seven more times over about two days, with growing gaps. Each retry is signed afresh, so its timestamp is current.
  • The same event can arrive more than once. Use id to ignore one you have already handled.
  • An endpoint that fails for a full day is switched off and we email you. Switch it back on in the dashboard; events from while it was off are not sent later.
  • Answering 410 Gone unsubscribes the endpoint at once.
  • Revoking an API key switches off the endpoints that key created.

Where a webhook can be sent

The address must be https on the standard port, with no username or password in it, and must resolve to a public internet address. Private, loopback and internal addresses are refused, when you add the endpoint and again on every delivery. An account may have up to 10 endpoints.

A webhook is a notification, not a record. The record of what was delivered and opened is the delivery log in your dashboard.

Zapier

A MyCaseViewer app for Zapier — triggers for each event above, and actions to create a case and send its link — is available by invitation while it is being tested. Write to us from the contact page and we will send you the invite. Until then, any tool that can receive a webhook or call an HTTP API works with everything on this page.