Skip to main content

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

FieldRequiredDescription
nameYesHuman-readable identifier for the client
dockIdNoRestrict client to a specific dock (recommended for security)
scopesNoArray of permission scopes (defaults to all available)
partyIdNoLink 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. 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

PrefixTypeExample
dyc_Machine Client IDdyc_epic_prod_abc123
dys_Machine Client Secretdys_live_xxxxxxxxxxxx
dyt_Machine Access Tokendyt_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:
  1. Generate new secret
  2. Update secrets manager with both old and new
  3. Deploy code that tries new secret, falls back to old
  4. Wait 24 hours
  5. 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:
  1. Insufficient scope for operation
  2. Client restricted to different dock
  3. 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