Skip to main content

Issue Machine Access Token

Exchanges machine client credentials for a short-lived access token via OAuth2 client credentials flow.
POST /v1/oauth/token

Authentication

This endpoint uses basic authentication with the client credentials in the request body (not the Authorization header).

Request Body

ParameterTypeRequiredDescription
grant_typestringRequiredMust be client_credentials
client_idstringRequiredMachine client ID (prefix: dyc_)
client_secretstringRequiredMachine client secret (prefix: dys_)
scopestringOptionalSpace-delimited scopes (must be subset of client scopes)

Example 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"
}

Response Fields

FieldTypeDescription
access_tokenstringShort-lived bearer token (prefix: dyt_)
token_typestringAlways Bearer
expires_inintegerToken lifetime in seconds (default: 3600)
scopestringGranted scopes for this token

Using the Token

Include the access token in the Authorization header for machine API calls:
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": "report.pdf"}'

Scope Handling

Requested vs. Granted Scopes: If you request a subset of the client’s scopes, you receive only those:
// Client has: ["artifacts:write", "artifacts:read", "policies:read"]
// You request: "artifacts:write"
// You receive: "artifacts:write"
If you omit the scope parameter, you receive all client scopes:
// You receive: "artifacts:write artifacts:read policies:read"
Insufficient Scope Error:
{
  "statusCode": 400,
  "message": "Invalid scope: requested 'artifacts:delete' not in client scopes",
  "error": "Bad Request"
}

Token Lifecycle

Default TTL: 3600 seconds (1 hour)
Max TTL: Configurable per organization (default 24 hours)
Renewal Strategy:
# Refresh 5 minutes before expiry
if datetime.now() >= expires_at - timedelta(minutes=5):
    token = issue_new_token()
Storage: Store tokens in memory only; never persist to disk or logs.

Error Handling

StatusCondition
400Invalid grant type or scope
401Invalid client credentials
403Client is deactivated (isActive: false)
Invalid Credentials:
{
  "statusCode": 401,
  "message": "Invalid client credentials",
  "error": "Unauthorized"
}
Deactivated Client:
{
  "statusCode": 403,
  "message": "Client is deactivated",
  "error": "Forbidden"
}

Security Considerations

Timing-Safe Comparison: The endpoint uses crypto.timingSafeEqual to prevent timing attacks on credential validation. Rate Limiting: Token issuance is rate-limited per client (10 requests/minute default). Audit Logging: Every token issuance is logged with client ID, timestamp, and scopes for security monitoring.

Code Examples

Python:
import requests
from datetime import datetime, timedelta

def get_machine_token(client_id, client_secret, scope=None):
    response = requests.post(
        'https://api.docyard.io/v1/oauth/token',
        json={
            'grant_type': 'client_credentials',
            'client_id': client_id,
            'client_secret': client_secret,
            'scope': scope
        }
    )
    response.raise_for_status()
    return response.json()

token_data = get_machine_token(
    'dyc_epic_prod_a1b2c3d4',
    'dys_live_efghijklmnop...',
    'artifacts:write'
)

print(f"Token: {token_data['access_token']}")
print(f"Expires in: {token_data['expires_in']} seconds")
Node.js:
async function getMachineToken(clientId, clientSecret, scope) {
  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: clientId,
      client_secret: clientSecret,
      scope
    })
  });
  
  if (!response.ok) {
    throw new Error(`Token request failed: ${response.statusText}`);
  }
  
  return response.json();
}

const tokenData = await getMachineToken(
  'dyc_epic_prod_a1b2c3d4',
  'dys_live_efghijklmnop...',
  'artifacts:write'
);

console.log(`Token: ${tokenData.access_token}`);
console.log(`Expires in: ${tokenData.expires_in} seconds`);