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.
smd_ and is shown only oncecurl -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.
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.
smd_ followed by 64 hex characters (68 chars total). Store it in your secrets manager or .env file.SENDMEDOCS_API_KEY) so your code can include it in the Authorization header.upload_url and upload test files to verify the end-to-end flow.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_..." }
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.
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.
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"
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 /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"
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/..."
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"
All errors return a JSON object with an error field:
{ "error": "Human-readable error message" }
| 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 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.
This section is optimized for AI-assisted development (LLM agents, copilots, code generators).
SENDMEDOCS_API_KEY), never hardcodeAuthorization: Bearer smd_... and Content-Type: application/jsonPOST /api/v1/requests with recipient info and itemsid for later lookups and file downloadsGET /api/v1/requests/:id to get item statuses and file metadataGET /api/v1/requests/:id/files/:fileId/download — use the presigned URL within 5 minuteserror field in all non-200 responses| 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... |
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
}
is_required defaults — items default to is_required: true. Set is_required: false explicitly if the item is optional.prefab and name — if prefab is set, the name and description fields are ignored. Use one approach or the other.position field on items to determine display order, and the request_item_id relationship to associate files with items.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.
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 }
}
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.