Machine Authentication Setup
Machine authentication enables automated systems to securely interact with Docyard using OAuth2 client credentials. This guide walks you through creating a machine client, issuing tokens, and making authenticated API calls.
Prerequisites
- Organization administrator access (API key with org-level permissions)
- A target dock already created under your organization
- A system capable of making HTTPS requests (server, CI/CD pipeline, etc.)
Overview
Step 1: Create a Machine Client
Machine clients are the OAuth2 “applications” that represent your automated systems. They are created at the organization level and scoped to specific docks and permissions.
Request
curl -X POST https://api.docyard.io/v1/organizations/{orgId}/machine-clients \
-H "Authorization: Bearer dk_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "epic-ehr-integration",
"dockId": "dock_01HQ3K...",
"scopes": ["artifacts:write", "artifacts:read"],
"partyId": "pty_01HQ3L..."
}'
Parameters
| Field | Required | Description |
|---|
name | Yes | Human-readable identifier for the client |
dockId | No | Restrict client to a specific dock (recommended for security) |
scopes | No | Array of permission scopes (defaults to all available) |
partyId | No | Link to a party record for audit trails |
Response
{
"id": "mc_01HQ3K9B2...",
"clientId": "dyc_epic_prod_a1b2c3d4",
"clientSecret": "dys_live_efghijklmnop...",
"name": "epic-ehr-integration",
"scopes": ["artifacts:write", "artifacts:read"],
"dockId": "dock_01HQ3K...",
"organizationId": "org_01HQ3J...",
"partyId": "pty_01HQ3L...",
"isActive": true,
"createdAt": "2024-03-01T12:00:00Z"
}
CRITICAL: The clientSecret is displayed only once upon creation. Store it immediately in a secrets manager. If lost, you must rotate the secret.
Security Considerations
Dock Scoping: Always specify dockId unless the client needs organization-wide access. This limits the blast radius if credentials are compromised.
Scope Selection: Request only the scopes you need:
- Ingestion-only systems:
["artifacts:write"]
- Retrieval-only systems:
["artifacts:read"]
- Full access:
["artifacts:write", "artifacts:read", "policies:read"]
Party Linkage: Link to a party record for comprehensive audit trails. This associates machine actions with real-world entities.
Step 2: Secure the Client Secret
The client secret is the “password” for your machine client. Protect it aggressively.
Recommended Storage
AWS Secrets Manager:
aws secretsmanager create-secret \
--name docyard/epic-ehr-integration \
--secret-string '{"clientId":"dyc_epic_prod_a1b2c3d4","clientSecret":"dys_live_..."}' \
--description "Docyard machine client for Epic EHR integration"
HashiCorp Vault:
vault kv put secret/docyard/epic \
clientId=dyc_epic_prod_a1b2c3d4 \
clientSecret=dys_live_efghijklmnop...
Environment Variables (development only):
export DOCYARD_CLIENT_ID="dyc_epic_prod_a1b2c3d4"
export DOCYARD_CLIENT_SECRET="dys_live_efghijklmnop..."
Never hardcode secrets in source code, commit them to version control, or log them. Use a secrets manager in production.
Step 3: Issue an Access Token
Access tokens are short-lived credentials (1 hour default) used to make API calls. Exchange your client credentials for a token.
Request
curl -X POST https://api.docyard.io/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "dyc_epic_prod_a1b2c3d4",
"client_secret": "dys_live_efghijklmnop...",
"scope": "artifacts:write"
}'
Response
{
"access_token": "dyt_live_qrstuvwx...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "artifacts:write"
}
Token Prefix Reference
| Prefix | Type | Example |
|---|
dyc_ | Machine Client ID | dyc_epic_prod_abc123 |
dys_ | Machine Client Secret | dys_live_xxxxxxxxxxxx |
dyt_ | Machine Access Token | dyt_live_abc123def456 |
Step 4: Make API Calls
Use the access token in the Authorization header for all machine API calls.
Upload an Artifact
curl -X POST https://api.docyard.io/v1/machine/docks/{dockId}/artifacts \
-H "Authorization: Bearer dyt_live_qrstuvwx..." \
-H "Content-Type: application/json" \
-d '{
"filename": "discharge-summary.pdf",
"metadata": {
"patientId": "12345",
"mrn": "MRN-2024-001",
"encounterDate": "2024-03-01",
"documentType": "discharge-summary"
}
}'
List Artifacts
curl https://api.docyard.io/v1/machine/docks/{dockId}/artifacts \
-H "Authorization: Bearer dyt_live_qrstuvwx..."
Response Example
{
"data": [
{
"id": "art_01HQ3K...",
"filename": "discharge-summary.pdf",
"contentType": "application/pdf",
"size": 245760,
"hash": "sha256:abc123...",
"storageKey": "docks/dock_01HQ3K/artifacts/...",
"metadata": {
"patientId": "12345",
"documentType": "discharge-summary"
},
"createdAt": "2024-03-01T12:30:00Z",
"updatedAt": "2024-03-01T12:30:00Z"
}
],
"meta": {
"total": 1,
"page": 1,
"pageSize": 20
}
}
Step 5: Handle Token Expiration
Tokens expire after 1 hour (configurable). Implement automatic renewal in your application.
Token Management Pattern (Python)
import time
import requests
from datetime import datetime, timedelta
class DocyardMachineClient:
def __init__(self, client_id: str, client_secret: str, token_url: str = "https://api.docyard.io/v1/oauth/token"):
self.client_id = client_id
self.client_secret = client_secret
self.token_url = token_url
self._access_token = None
self._expires_at = None
def _get_token(self) -> str:
"""Get or refresh the access token."""
# Refresh 5 minutes before expiry to avoid race conditions
if self._access_token and self._expires_at and datetime.now() < self._expires_at - timedelta(minutes=5):
return self._access_token
response = requests.post(
self.token_url,
json={
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret,
"scope": "artifacts:write"
}
)
response.raise_for_status()
data = response.json()
self._access_token = data["access_token"]
expires_in = data.get("expires_in", 3600)
self._expires_at = datetime.now() + timedelta(seconds=expires_in)
print(f"Token refreshed. Expires at: {self._expires_at}")
return self._access_token
def create_artifact(self, dock_id: str, filename: str, metadata: dict = None):
"""Upload an artifact with automatic token management."""
token = self._get_token()
response = requests.post(
f"https://api.docyard.io/v1/machine/docks/{dock_id}/artifacts",
headers={
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
},
json={
"filename": filename,
"metadata": metadata or {}
}
)
response.raise_for_status()
return response.json()
# Usage
client = DocyardMachineClient(
client_id="dyc_epic_prod_a1b2c3d4",
client_secret="dys_live_efghijklmnop..."
)
# Token is automatically managed
artifact = client.create_artifact(
dock_id="dock_01HQ3K...",
filename="lab-results.pdf",
metadata={"patientId": "12345", "testType": "blood-panel"}
)
Token Management Pattern (Node.js)
class DocyardMachineClient {
constructor(clientId, clientSecret) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.accessToken = null;
this.expiresAt = null;
}
async getToken() {
// Refresh 5 minutes before expiry
if (this.accessToken && this.expiresAt && Date.now() < this.expiresAt - 300000) {
return this.accessToken;
}
const response = await fetch('https://api.docyard.io/v1/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
scope: 'artifacts:write'
})
});
if (!response.ok) {
throw new Error(`Token request failed: ${response.statusText}`);
}
const data = await response.json();
this.accessToken = data.access_token;
this.expiresAt = Date.now() + (data.expires_in * 1000);
console.log(`Token refreshed. Expires at: ${new Date(this.expiresAt)}`);
return this.accessToken;
}
async createArtifact(dockId, filename, metadata = {}) {
const token = await this.getToken();
const response = await fetch(
`https://api.docyard.io/v1/machine/docks/${dockId}/artifacts`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ filename, metadata })
}
);
if (!response.ok) {
throw new Error(`Artifact creation failed: ${response.statusText}`);
}
return response.json();
}
}
// Usage
const client = new DocyardMachineClient(
'dyc_epic_prod_a1b2c3d4',
'dys_live_efghijklmnop...'
);
const artifact = await client.createArtifact(
'dock_01HQ3K...',
'lab-results.pdf',
{ patientId: '12345', testType: 'blood-panel' }
);
Production Best Practices
1. Secret Rotation
Rotate client secrets quarterly or after any security incident:
# Generate new secret
curl -X POST https://api.docyard.io/v1/organizations/{orgId}/machine-clients/{clientId}/rotate \
-H "Authorization: Bearer dk_live_..."
Rotation Strategy:
- Generate new secret
- Update secrets manager with both old and new
- Deploy code that tries new secret, falls back to old
- Wait 24 hours
- Revoke old secret
2. Error Handling
Handle these specific error cases:
def handle_api_error(response):
if response.status_code == 401:
# Token expired - refresh and retry
token = client.refresh_token()
return retry_request(token)
elif response.status_code == 403:
# Insufficient scope - log and alert
log_security_event("Machine client scope violation", response.json())
raise InsufficientScopeError()
elif response.status_code == 429:
# Rate limited - exponential backoff
retry_after = int(response.headers.get('Retry-After', 60))
time.sleep(retry_after)
return retry_request()
else:
response.raise_for_status()
3. Observability
Implement comprehensive logging:
import structlog
logger = structlog.get_logger()
def log_machine_action(action, dock_id, artifact_id=None, error=None):
logger.info(
"machine_action",
action=action,
client_id=CLIENT_ID, # Never log the secret!
dock_id=dock_id,
artifact_id=artifact_id,
error=str(error) if error else None,
timestamp=datetime.utcnow().isoformat()
)
4. CI/CD Integration
For deployment pipelines that push artifacts:
# .github/workflows/deploy-docs.yml
jobs:
deploy:
steps:
- name: Get Docyard Token
id: token
run: |
TOKEN=$(aws secretsmanager get-secret-value \
--secret-id docyard/cicd-client \
--query SecretString \
--output text | jq -r '.clientSecret')
ACCESS_TOKEN=$(curl -X POST https://api.docyard.io/v1/oauth/token \
-H "Content-Type: application/json" \
-d "{\"grant_type\":\"client_credentials\",\"client_id\":\"${CLIENT_ID}\",\"client_secret\":\"${TOKEN}\"}" \
| jq -r '.access_token')
echo "::set-output name=token::$ACCESS_TOKEN"
env:
CLIENT_ID: ${{ secrets.DOCYARD_CLIENT_ID }}
- name: Upload Build Artifacts
run: |
curl -X POST https://api.docyard.io/v1/machine/docks/${{ env.DOCK_ID }}/artifacts \
-H "Authorization: Bearer ${{ steps.token.outputs.token }}" \
-F "[email protected]" \
-F "metadata={\"buildId\":\"${{ github.run_id }}\"}"
Industry Examples
Healthcare: EHR Integration
System: Epic EHR pushing discharge summaries to Docyard
# Epic EHR integration
class EpicDocyardIntegration:
def __init__(self):
self.client = DocyardMachineClient(
client_id="dyc_epic_prod_a1b2c3d4",
client_secret=get_secret("epic/docyard-secret"),
scope="artifacts:write"
)
def on_discharge(self, patient_id, encounter_id):
"""Triggered when patient is discharged."""
# Generate discharge summary PDF from Epic
pdf = self.generate_discharge_summary(patient_id, encounter_id)
# Upload to Docyard
artifact = self.client.create_artifact(
dock_id="dock_metro_general",
filename=f"discharge-{patient_id}-{encounter_id}.pdf",
metadata={
"patientId": patient_id,
"mrn": self.get_mrn(patient_id),
"encounterId": encounter_id,
"dischargeDate": datetime.now().isoformat(),
"documentType": "discharge-summary"
}
)
# Notify patient via Epic's messaging system
self.notify_patient(patient_id, artifact["id"])
Financial Services: Loan Origination
System: Automated loan document ingestion from LOS
class LOSIntegration:
def __init__(self):
self.client = DocyardMachineClient(
client_id="dyc_los_integration_xyz789",
client_secret=get_secret("los/docyard-secret"),
scope="artifacts:write"
)
def process_loan_package(self, loan_id, documents):
"""Process documents from loan origination system."""
artifact_ids = []
for doc in documents:
artifact = self.client.create_artifact(
dock_id="dock_mortgage_ops",
filename=doc["name"],
metadata={
"loanId": loan_id,
"borrowerName": doc["borrower_name"],
"documentType": doc["type"],
"fannieMaeForm": doc.get("fnm_form"),
"freddieMacForm": doc.get("flm_form")
}
)
artifact_ids.append(artifact["id"])
# Create bulk retrieval job for lender
self.create_lender_retrieval_job(loan_id, artifact_ids)
Real Estate: Title Production
System: Automated title policy generation and distribution
class TitleProductionSystem:
def __init__(self):
self.client = DocyardMachineClient(
client_id="dyc_title_prod_abc123",
client_secret=get_secret("title/docyard-secret"),
scope="artifacts:write artifacts:read"
)
def generate_and_distribute(self, order_id):
"""Generate title policy and distribute to parties."""
# Generate PDF
policy_pdf = self.generate_title_policy(order_id)
# Upload to Docyard
artifact = self.client.create_artifact(
dock_id="dock_title_policies",
filename=f"title-policy-{order_id}.pdf",
metadata={
"orderId": order_id,
"propertyAddress": self.get_property_address(order_id),
"buyer": self.get_buyer_name(order_id),
"seller": self.get_seller_name(order_id),
"premium": self.calculate_premium(order_id)
}
)
# Policy is automatically available to lender and buyer via access recipes
self.log_distribution(order_id, artifact["id"])
Troubleshooting
Issue: 401 Unauthorized
Cause: Invalid or expired token
Solution:
if response.status_code == 401:
token = client.get_token(force_refresh=True)
return retry_request(token)
Issue: 403 Forbidden
Cause:
- Insufficient scope for operation
- Client restricted to different dock
- Client deactivated
Solution:
error = response.json()
if "scope" in error["message"].lower():
# Request new token with correct scope
token = client.get_token(scope="artifacts:write")
elif "dock" in error["message"].lower():
# Verify dock ID matches client configuration
log_error(f"Dock mismatch: requested {dock_id}, client configured for {client_dock_id}")
Issue: 429 Rate Limited
Cause: Too many requests from the same client
Solution:
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 60))
time.sleep(retry_after)
return retry_request()
Issue: Token Refresh Failing
Cause: Client secret rotated or client deactivated
Solution:
- Check client status in dashboard
- Verify secret hasn’t been rotated
- Review audit logs for revocation events
Next Steps