SendMeDocs / Docs AI Assistants External API Webhooks MCP Server Zapier & Automations

External API

SendMeDocs provides a RESTful API for programmatically creating document requests, checking their status, downloading submitted files, and cancelling requests. This lets you integrate document collection into your existing workflows without using the dashboard.

Quick start

  1. Go to Settings > API Keys in the dashboard (owner or admin only)
  2. Click Create API Key, give it a name
  3. Copy the key immediately — it starts with smd_ and is shown only once
  4. Make your first request:
curl -X POST https://your-instance.com/api/v1/requests \
  -H "Authorization: Bearer smd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "recipient_name": "Jane Doe",
    "recipient_contact": "[email protected]",
    "message": "Please upload your documents.",
    "items": [
      { "prefab": "photo_id" },
      { "name": "Tax Return", "description": "Last 2 years", "allow_multiple": true }
    ]
  }'

The response includes the request id and an upload_url path that the recipient will use to upload their documents.

Human operator checklist

Complete these steps before an AI (or developer) can implement the integration. These require dashboard access or organizational decisions that can't be generated by code alone.

Before implementation

After implementation

Authentication

All API requests require an Authorization header with a Bearer token:

Authorization: Bearer smd_a1b2c3d4e5f6...

API keys are org-scoped — every request made with a key operates within that organization. Keys are created in the dashboard by owners or admins.

Key format: smd_ prefix + 32 random bytes (hex-encoded) = 68 characters total. The key is stored as a SHA-256 hash on the server. The first 12 characters (key_prefix) are shown in the dashboard for identification.

Key limits: Maximum 5 API keys per organization.

If the key is missing or invalid, the API returns:

HTTP 401
{ "error": "Missing or invalid API key. Use: Authorization: Bearer smd_..." }

Rate limits

Each API key is limited to 60 requests per minute. When exceeded:

HTTP 429
{ "error": "Rate limit exceeded. Maximum 60 requests per minute." }

The rate limit uses a sliding window. Wait briefly and retry.

Base URL

All endpoints are under /api/v1/:

https://your-instance.com/api/v1/requests              GET (list) / POST (create)
https://your-instance.com/api/v1/requests/:id           GET (detail)
https://your-instance.com/api/v1/requests/:id/files/:fileId/download  GET
https://your-instance.com/api/v1/requests/:id/cancel    POST

See also: MCP Server for AI assistant integration.

Endpoints

List requests

GET /api/v1/requests

Returns a paginated list of document requests for your organization.

Query parameters:

Parameter Type Default Description
status string Filter by status: pending, completed, or expired
search string Search by recipient name or contact (case-insensitive partial match)
limit number 20 Number of results to return (1-100)
offset number 0 Number of results to skip (for pagination)

Response (200):

{
  "requests": [
    {
      "id": "019471a2-b3c4-7d5e-8f6a-1234567890ab",
      "recipient_name": "Jane Doe",
      "recipient_contact": "[email protected]",
      "status": "pending",
      "created_at": "2026-02-10T12:00:00.000Z",
      "expires_at": null,
      "completed_at": null
    }
  ],
  "total": 42
}

The total field is the count of all matching requests (ignoring limit/offset), useful for building pagination UI.

curl example:

# List all requests
curl https://your-instance.com/api/v1/requests \
  -H "Authorization: Bearer smd_your_key_here"

# Filter by status with pagination
curl "https://your-instance.com/api/v1/requests?status=pending&limit=10&offset=0" \
  -H "Authorization: Bearer smd_your_key_here"

# Search by recipient
curl "https://your-instance.com/api/v1/requests?search=jane" \
  -H "Authorization: Bearer smd_your_key_here"

Create request

POST /api/v1/requests

Creates a new document request. Sends an upload link email to the recipient automatically.

Request body:

{
  "recipient_name": "Jane Doe",
  "recipient_contact": "[email protected]",
  "message": "Please upload your documents for loan processing.",
  "expires_at": "2026-03-10T00:00:00.000Z",
  "items": [
    { "prefab": "photo_id", "is_required": true },
    { "prefab": "proof_of_address" },
    { "name": "Tax Return", "description": "Last 2 years", "is_required": true, "allow_multiple": true }
  ]
}

Body fields:

Field Type Required Constraints Description
recipient_name string Yes 1-255 chars Recipient's display name
recipient_contact string Yes 1-255 chars Email or US phone number. Accepts common formats: (555) 123-4567, 555-123-4567, 15551234567. Only US numbers are supported.
message string No max 1000 chars Custom message included in the upload link email
expires_at string No ISO 8601 When the request expires. Omit for no expiration.
locale string No One of: en, es, fr, de, pt, zh, ja, ko Language for the upload portal and recipient emails. Defaults to the org's configured default locale, which defaults to en.
items array Yes 1-20 items Document items to request (see below)

Item fields:

Each item is either a prefab (predefined document type) or a custom item:

Field Type Required Default Description
prefab string No One of: photo_id, proof_of_address, proof_of_income, w9. Sets name and description automatically.
name string No "Document" Custom item name (max 255 chars). Ignored if prefab is set.
description string No null Custom item description (max 1000 chars). Ignored if prefab is set.
is_required boolean No true Whether the recipient must upload this item
allow_multiple boolean No false Whether the recipient can upload multiple files for this item. Ignored if prefab is set (prefabs default to false).

Response (200):

{
  "id": "019471a2-b3c4-7d5e-8f6a-1234567890ab",
  "token": "abc123def456...",
  "status": "pending",
  "upload_url": "/u/abc123def456...",
  "items": [
    {
      "id": "019471a2-...",
      "name": "Photo ID",
      "description": "Driver's license, passport, or state ID",
      "is_required": true,
      "allow_multiple": false,
      "position": 0,
      "status": "pending"
    },
    {
      "id": "019471a3-...",
      "name": "Proof of Address",
      "description": "Utility bill or bank statement (within 90 days)",
      "is_required": true,
      "allow_multiple": false,
      "position": 1,
      "status": "pending"
    },
    {
      "id": "019471a4-...",
      "name": "Tax Return",
      "description": "Last 2 years",
      "is_required": true,
      "allow_multiple": true,
      "position": 2,
      "status": "pending"
    }
  ],
  "created_at": "2026-02-10T12:00:00.000Z"
}

curl example:

curl -X POST https://your-instance.com/api/v1/requests \
  -H "Authorization: Bearer smd_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "recipient_name": "Jane Doe",
    "recipient_contact": "[email protected]",
    "items": [
      { "prefab": "photo_id" },
      { "prefab": "proof_of_address" }
    ]
  }'

Get request detail

GET /api/v1/requests/:id

Returns the full detail of a request including items, their statuses, and any uploaded files.

Response (200):

{
  "id": "019471a2-b3c4-7d5e-8f6a-1234567890ab",
  "token": "abc123def456...",
  "recipient_name": "Jane Doe",
  "recipient_contact": "[email protected]",
  "recipient_contact_type": "email",
  "status": "completed",
  "message": "Please upload your documents.",
  "revision_count": 0,
  "created_at": "2026-02-10T12:00:00.000Z",
  "expires_at": null,
  "completed_at": "2026-02-11T09:30:00.000Z",
  "upload_url": "/u/abc123def456...",
  "items": [
    {
      "id": "019471a2-...",
      "name": "Photo ID",
      "description": "Driver's license, passport, or state ID",
      "is_required": true,
      "allow_multiple": false,
      "position": 0,
      "status": "submitted",
      "revision_note": null,
      "files": [
        {
          "id": "019471b5-...",
          "filename": "drivers_license.jpg",
          "file_size": 2048576,
          "content_type": "image/jpeg",
          "created_at": "2026-02-11T09:25:00.000Z"
        }
      ]
    }
  ]
}

Request status values:

Status Meaning
pending Waiting for the recipient to upload documents
completed Recipient has submitted all required documents
expired Request passed its expiry date or was cancelled

Item status values:

Status Meaning
pending No file uploaded yet
submitted File uploaded by the recipient
approved Approved during revision review (locked — cannot be re-uploaded)
rejected Rejected during revision review (must be re-uploaded)

curl example:

curl https://your-instance.com/api/v1/requests/019471a2-b3c4-7d5e-8f6a-1234567890ab \
  -H "Authorization: Bearer smd_your_key_here"

Download file

GET /api/v1/requests/:id/files/:fileId/download

Returns a presigned S3 download URL for a specific file. The URL is valid for 5 minutes.

Response (200):

{
  "download_url": "https://s3.example.com/org123/req456/abcdef.jpg?X-Amz-...",
  "filename": "drivers_license.jpg"
}

The download_url is a temporary presigned URL. Download the file immediately or within the 5-minute window. Do not store the URL — request a new one when needed.

Retention: Files from completed requests are automatically deleted after the plan's retention period (API: 3 days, Standard: 7 days, Pro: 30 days, Enterprise: unlimited). Orgs can configure shorter retention in Settings. Once deleted, this endpoint returns 404. Download files promptly after request completion.

curl example:

# Get the download URL
curl https://your-instance.com/api/v1/requests/019471a2-.../files/019471b5-.../download \
  -H "Authorization: Bearer smd_your_key_here"

# Download the file using the presigned URL
curl -o drivers_license.jpg "https://s3.example.com/..."

Cancel request

POST /api/v1/requests/:id/cancel

Cancels a pending request. Only requests with status: "pending" can be cancelled. The request status is set to expired.

Response (200):

{ "success": true }

Error — not pending:

HTTP 400
{ "error": "Can only cancel pending requests" }

curl example:

curl -X POST https://your-instance.com/api/v1/requests/019471a2-.../cancel \
  -H "Authorization: Bearer smd_your_key_here"

Error responses

All errors return a JSON object with an error field:

{ "error": "Human-readable error message" }

Status codes

Status Meaning
200 Success
400 Validation error (bad input, invalid state transition)
401 Invalid or missing API key
404 Resource not found (or does not belong to your organization)
429 Rate limit exceeded (60 requests per minute per key)

Prefab item types

Prefab items provide standardized names and descriptions for common document types. Use the prefab field in the items array instead of specifying name and description manually.

Prefab key Name Description allow_multiple
photo_id Photo ID Driver's license, passport, or state ID false
proof_of_address Proof of Address Utility bill or bank statement (within 90 days) false
proof_of_income Proof of Income Pay stubs, tax returns, or employment letter false
w9 W-9 Completed W-9 tax form false

When using a prefab, is_required can still be overridden (defaults to true). The allow_multiple and name/description fields come from the prefab definition.

AI integration guide

This section is optimized for AI-assisted development (LLM agents, copilots, code generators).

Implementation checklist — API client

  1. Store the API key securely — read from environment variable (e.g. SENDMEDOCS_API_KEY), never hardcode
  2. Set headers on every request: Authorization: Bearer smd_... and Content-Type: application/json
  3. Create a request via POST /api/v1/requests with recipient info and items
  4. Handle the response — store the id for later lookups and file downloads
  5. Poll or use webhooks to know when the request is completed (see docs/webhooks.md for webhook setup)
  6. Fetch request detail via GET /api/v1/requests/:id to get item statuses and file metadata
  7. Download files via GET /api/v1/requests/:id/files/:fileId/download — use the presigned URL within 5 minutes
  8. Handle errors — check for error field in all non-200 responses
  9. Respect rate limits — implement backoff on 429 responses

Inputs your integration needs from the human/system

Input Source Format Example
API key Dashboard (shown once at creation) smd_ + 64 hex chars smd_a1b2c3d4e5f6...
Base URL Your SendMeDocs instance HTTPS URL https://sendmedocs.com
Webhook secret (optional) Dashboard (shown once at endpoint creation) 64-char hex string a1b2c3d4e5f6...

TypeScript types

Use these types when generating an API client:

// ============================================================================
// Query params — GET /api/v1/requests
// ============================================================================

interface ListRequestsParams {
  status?: "pending" | "completed" | "expired"
  search?: string               // partial match on name/email
  limit?: number                // 1-100, default 20
  offset?: number               // default 0
}

// ============================================================================
// Response — GET /api/v1/requests
// ============================================================================

interface ListRequestsResponse {
  requests: ListRequestItem[]
  total: number                  // total matching (ignoring limit/offset)
}

interface ListRequestItem {
  id: string
  recipient_name: string
  recipient_contact: string
  status: "pending" | "completed" | "expired"
  created_at: string             // ISO 8601
  expires_at: string | null
  completed_at: string | null
}

// ============================================================================
// Request body — POST /api/v1/requests
// ============================================================================

interface CreateRequestBody {
  recipient_name: string       // 1-255 chars, required
  recipient_contact: string    // 1-255 chars, required — email or US phone number
  message?: string             // max 1000 chars
  expires_at?: string          // ISO 8601 datetime
  locale?: "en" | "es" | "fr" | "de" | "pt" | "zh" | "ja" | "ko"  // upload portal + email language
  items: CreateRequestItem[]   // 1-20 items
}

interface CreateRequestItem {
  prefab?: "photo_id" | "proof_of_address" | "proof_of_income" | "w9"
  name?: string                // max 255 chars, ignored if prefab is set
  description?: string         // max 1000 chars, ignored if prefab is set
  is_required?: boolean        // default: true
  allow_multiple?: boolean     // default: false, ignored if prefab is set
}

// ============================================================================
// Response — POST /api/v1/requests
// ============================================================================

interface CreateRequestResponse {
  id: string                   // UUID v7
  token: string                // upload token
  status: "pending"
  upload_url: string           // "/u/{token}"
  items: RequestItemSummary[]
  created_at: string           // ISO 8601
}

interface RequestItemSummary {
  id: string
  name: string
  description: string | null
  is_required: boolean
  allow_multiple: boolean
  position: number
  status: "pending"
}

// ============================================================================
// Response — GET /api/v1/requests/:id
// ============================================================================

interface GetRequestResponse {
  id: string
  token: string
  recipient_name: string
  recipient_contact: string
  recipient_contact_type: "email" | "sms"
  status: "pending" | "completed" | "expired"
  message: string | null
  revision_count: number
  created_at: string           // ISO 8601
  expires_at: string | null    // ISO 8601
  completed_at: string | null  // ISO 8601
  upload_url: string           // "/u/{token}"
  items: RequestItemDetail[]
}

interface RequestItemDetail {
  id: string
  name: string
  description: string | null
  is_required: boolean
  allow_multiple: boolean
  position: number
  status: "pending" | "submitted" | "approved" | "rejected"
  revision_note: string | null
  files: FileDetail[]
}

interface FileDetail {
  id: string
  filename: string
  file_size: number            // bytes
  content_type: string         // MIME type
  created_at: string           // ISO 8601
}

// ============================================================================
// Response — GET /api/v1/requests/:id/files/:fileId/download
// ============================================================================

interface DownloadResponse {
  download_url: string         // presigned S3 URL, valid for 5 minutes
  filename: string
}

// ============================================================================
// Response — POST /api/v1/requests/:id/cancel
// ============================================================================

interface CancelResponse {
  success: true
}

// ============================================================================
// Error response — all endpoints
// ============================================================================

interface ErrorResponse {
  error: string
}

Common mistakes to avoid

Example integration flow

A typical integration creates a request, listens for completion via webhook, then downloads the files:

1. Your system  ──POST /api/v1/requests──>  SendMeDocs
                <── 200 { id, upload_url } ──

2. SendMeDocs sends upload link email to recipient
3. Recipient uploads documents via upload portal

4. SendMeDocs ──POST webhook──> Your endpoint
               { type: "request.completed", data: { id, items } }

5. Your system ──GET /api/v1/requests/:id──> SendMeDocs
               <── 200 { items: [{ files: [...] }] } ──

6. Your system ──GET /api/v1/requests/:id/files/:fileId/download──> SendMeDocs
               <── 200 { download_url } ──

7. Your system ──GET download_url──> S3
               <── file bytes ──

Without webhooks (polling approach):

1. Create the request (POST /api/v1/requests)
2. Store the request id
3. Periodically GET /api/v1/requests/:id
4. When status changes to "completed", download files
5. When status changes to "expired", handle expiration

See docs/webhooks.md for webhook setup, signature verification, and event types.

Full working example (Node.js)

const API_KEY = process.env.SENDMEDOCS_API_KEY;
const BASE_URL = process.env.SENDMEDOCS_URL; // e.g. "https://sendmedocs.com"

async function createRequest(recipient) {
  const res = await fetch(`${BASE_URL}/api/v1/requests`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      recipient_name: recipient.name,
      recipient_contact: recipient.email,
      message: "Please upload the required documents.",
      items: [
        { prefab: "photo_id" },
        { prefab: "proof_of_address" },
        {
          name: "Signed Agreement",
          description: "The signed service agreement we sent you",
          is_required: true,
        },
      ],
    }),
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`Create request failed: ${err.error}`);
  }

  return res.json(); // { id, token, status, upload_url, items, created_at }
}

async function getRequest(requestId) {
  const res = await fetch(`${BASE_URL}/api/v1/requests/${requestId}`, {
    headers: { Authorization: `Bearer ${API_KEY}` },
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`Get request failed: ${err.error}`);
  }

  return res.json();
}

async function downloadFile(requestId, fileId) {
  const res = await fetch(
    `${BASE_URL}/api/v1/requests/${requestId}/files/${fileId}/download`,
    { headers: { Authorization: `Bearer ${API_KEY}` } }
  );

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`Download failed: ${err.error}`);
  }

  const { download_url, filename } = await res.json();

  // Download the actual file from the presigned URL
  const fileRes = await fetch(download_url);
  const buffer = await fileRes.arrayBuffer();

  return { filename, buffer };
}

async function cancelRequest(requestId) {
  const res = await fetch(`${BASE_URL}/api/v1/requests/${requestId}/cancel`, {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}` },
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(`Cancel failed: ${err.error}`);
  }

  return res.json(); // { success: true }
}

MCP server

SendMeDocs also provides an MCP server for AI assistants like Claude Desktop and Cursor. It uses the same API key authentication. See the MCP docs for setup instructions.