Kulcare Docs

API Response Structure Guidelines

Core Envelope

Every API response must use this envelope structure:

{
  "data": <object | array | null>,
  "error": <object | null>,
  "meta": <object>
}

Rules

  1. No success field — HTTP status codes indicate success (2xx) or failure (4xx/5xx). The presence of error is sufficient for client-side branching.

  2. data is always present — Contains the response payload on success, null on error.

  3. error is only present on failure — Never include an empty error object on success.

  4. meta is always present — Contains request metadata. At minimum, include request_id.

  5. Use snake_case for all keys — No camelCase. This applies to field names, nested objects, and meta fields.

  6. Timestamps use ISO 8601 format — Always include timezone: 2024-01-07T10:30:00Z

Success Response (Single Resource)

{
  "data": {
    "id": "123e4567-e89b-12d3-a456-426614174000",
    "email": "user@example.com",
    "full_name": "John Doe",
    "created_at": "2024-01-07T10:30:00Z",
    "updated_at": "2024-01-07T10:30:00Z"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Success Response (Collection)

{
  "data": [
    { "id": "...", "email": "..." },
    { "id": "...", "email": "..." }
  ],
  "meta": {
    "request_id": "req_abc123",
    "pagination": {
      "total_entries": 100,
      "total_pages": 4,
      "current_page": 1,
      "per_page": 25
    }
  }
}

Error Response

{
  "data": null,
  "error": {
    "code": "validation_failed",
    "message": "Human-readable error description",
    "details": [
      { "field": "email", "message": "has already been taken" },
      { "field": "phone", "message": "is invalid format" }
    ]
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Error Codes

Use consistent, lowercase, underscore-separated error codes:

  • validation_failed — Request validation errors (422)
  • not_found — Resource does not exist (404)
  • unauthorized — Authentication required (401)
  • forbidden — Insufficient permissions (403)
  • conflict — Resource state conflict (409)
  • rate_limited — Too many requests (429)
  • internal_error — Server error (500)

Authentication & Authorization

Authentication Methods

Support multiple authentication methods with clear precedence:

  1. Bearer Token (Primary) — JWT or opaque tokens
  2. API Key — For server-to-server integrations
  3. Session Cookie — For browser-based apps

Bearer Token Authentication

Request Header:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

API Key Authentication

Request Header:

X-Api-Key: kulcare_live_abc123def456

API key format: kulcare_{environment}_{random} where environment is live or test.

Login Flow

Request: POST /auth/login

{
  "data": {
    "email": "doctor@clinic.com",
    "password": "securepassword123"
  }
}

Response: HTTP 200

{
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "ref_abc123def456",
    "token_type": "Bearer",
    "expires_in": 3600,
    "expires_at": "2024-01-07T11:30:00Z",
    "user": {
      "id": "usr_789",
      "email": "doctor@clinic.com",
      "full_name": "Dr. Raj Patel",
      "role": "provider"
    }
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Token Refresh

Request: POST /auth/refresh

{
  "data": {
    "refresh_token": "ref_abc123def456"
  }
}

Response: HTTP 200

{
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "refresh_token": "ref_xyz789ghi012",
    "token_type": "Bearer",
    "expires_in": 3600,
    "expires_at": "2024-01-07T12:30:00Z"
  },
  "meta": {
    "request_id": "req_def456"
  }
}

Logout

Request: POST /auth/logout

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response: HTTP 200

{
  "data": {
    "logged_out": true
  },
  "meta": {
    "request_id": "req_ghi789"
  }
}

Authentication Errors

Invalid Credentials: HTTP 401

{
  "data": null,
  "error": {
    "code": "invalid_credentials",
    "message": "Email or password is incorrect"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Token Expired: HTTP 401

{
  "data": null,
  "error": {
    "code": "token_expired",
    "message": "Access token has expired",
    "details": [
      { "field": "expired_at", "message": "2024-01-07T10:30:00Z" }
    ]
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Token Invalid: HTTP 401

{
  "data": null,
  "error": {
    "code": "token_invalid",
    "message": "Access token is malformed or has been revoked"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Multi-Factor Authentication

Step 1: Login returns MFA challenge

Response: HTTP 200

{
  "data": {
    "mfa_required": true,
    "mfa_token": "mfa_abc123",
    "mfa_methods": [
      { "type": "totp", "hint": "Authenticator app" },
      { "type": "sms", "hint": "****1234" }
    ],
    "expires_at": "2024-01-07T10:35:00Z"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Step 2: Submit MFA code

Request: POST /auth/mfa/verify

{
  "data": {
    "mfa_token": "mfa_abc123",
    "mfa_method": "totp",
    "code": "123456"
  }
}

Response: HTTP 200 (same as login success)

Authorization Headers

Include authorization context in response headers:

X-Auth-User-Id: usr_789
X-Auth-Facility-Id: fac_456
X-Auth-Role: provider
X-Auth-Permissions: patients:read,patients:write,encounters:read

Permission Denied

Response: HTTP 403

{
  "data": null,
  "error": {
    "code": "forbidden",
    "message": "You do not have permission to perform this action",
    "details": [
      { "field": "required_permission", "message": "patients:delete" },
      { "field": "your_permissions", "message": "patients:read,patients:write" }
    ]
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Resource-Level Authorization

When access is denied to a specific resource:

Response: HTTP 403

{
  "data": null,
  "error": {
    "code": "forbidden",
    "message": "You do not have access to this patient record",
    "details": [
      { "field": "resource_type", "message": "patient" },
      { "field": "resource_id", "message": "pat_789" },
      { "field": "reason", "message": "Patient belongs to different facility" }
    ]
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Current User Endpoint

Request: GET /auth/me

Response: HTTP 200

{
  "data": {
    "id": "usr_789",
    "email": "doctor@clinic.com",
    "full_name": "Dr. Raj Patel",
    "role": "provider",
    "permissions": [
      "patients:read",
      "patients:write",
      "encounters:read",
      "encounters:write",
      "prescriptions:write"
    ],
    "facilities": [
      { "id": "fac_456", "name": "Downtown Clinic", "role": "admin" },
      { "id": "fac_789", "name": "Westside Medical", "role": "provider" }
    ],
    "current_facility_id": "fac_456",
    "preferences": {
      "timezone": "Asia/Kolkata",
      "locale": "en-IN",
      "date_format": "DD/MM/YYYY"
    },
    "created_at": "2023-06-15T09:00:00Z",
    "last_login_at": "2024-01-07T10:30:00Z"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Switch Facility Context

Request: POST /auth/switch-facility

{
  "data": {
    "facility_id": "fac_789"
  }
}

Response: HTTP 200

{
  "data": {
    "current_facility_id": "fac_789",
    "facility": {
      "id": "fac_789",
      "name": "Westside Medical",
      "role": "provider"
    },
    "permissions": [
      "patients:read",
      "encounters:read",
      "encounters:write"
    ]
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

API Key Management

List API Keys: GET /auth/api-keys

{
  "data": [
    {
      "id": "key_abc123",
      "name": "Production Integration",
      "prefix": "kulcare_live_abc",
      "permissions": ["patients:read", "encounters:read"],
      "last_used_at": "2024-01-07T09:15:00Z",
      "created_at": "2023-12-01T10:00:00Z",
      "expires_at": null
    }
  ],
  "meta": {
    "request_id": "req_abc123"
  }
}

Create API Key: POST /auth/api-keys

{
  "data": {
    "name": "Mobile App Integration",
    "permissions": ["patients:read", "encounters:read"],
    "expires_at": "2025-01-07T00:00:00Z"
  }
}

Response: HTTP 201 (only time full key is shown)

{
  "data": {
    "id": "key_def456",
    "name": "Mobile App Integration",
    "key": "kulcare_live_def456ghi789jkl012mno345",
    "prefix": "kulcare_live_def",
    "permissions": ["patients:read", "encounters:read"],
    "created_at": "2024-01-07T10:30:00Z",
    "expires_at": "2025-01-07T00:00:00Z"
  },
  "meta": {
    "request_id": "req_abc123",
    "warning": "Store this key securely. It will not be shown again."
  }
}

Authentication Rules

  1. Access tokens expire in 1 hour — Short-lived for security

  2. Refresh tokens expire in 30 days — Rotate on each use

  3. Revoke all tokens on password change — Force re-authentication

  4. Rate limit auth endpoints aggressively — 20 requests per 15 minutes

  5. Log all authentication events — Success, failure, token refresh, logout

  6. Include X-Auth-* headers in responses — Helps debugging

  7. Never include passwords in responses — Even masked

  8. API keys are facility-scoped — Cannot access other facilities' data

  9. Support token revocation — Both individual and all tokens for user

  10. MFA tokens expire in 5 minutes — Short window for code entry


Webhook Delivery

Webhook Registration

Request: POST /webhooks

{
  "data": {
    "url": "https://myapp.com/webhooks/kulcare",
    "events": [
      "patient.created",
      "patient.updated",
      "encounter.completed",
      "prescription.created"
    ],
    "secret": "whsec_abc123def456",
    "active": true
  }
}

Response: HTTP 201

{
  "data": {
    "id": "whk_789",
    "url": "https://myapp.com/webhooks/kulcare",
    "events": [
      "patient.created",
      "patient.updated",
      "encounter.completed",
      "prescription.created"
    ],
    "secret_preview": "whsec_abc...456",
    "active": true,
    "created_at": "2024-01-07T10:30:00Z"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Webhook Payload Structure

All webhook payloads use this structure:

{
  "id": "evt_abc123def456",
  "type": "patient.created",
  "api_version": "2.0",
  "created_at": "2024-01-07T10:30:00Z",
  "data": {
    "object": {
      "id": "pat_789",
      "full_name": "Jane Smith",
      "email": "jane@example.com",
      "created_at": "2024-01-07T10:30:00Z"
    },
    "previous_attributes": null
  },
  "facility_id": "fac_456",
  "request_id": "req_abc123"
}

Event Types

EventDescription
patient.createdNew patient registered
patient.updatedPatient record modified
patient.deletedPatient record deleted
encounter.createdNew encounter started
encounter.updatedEncounter modified
encounter.completedEncounter marked complete
prescription.createdNew prescription created
prescription.dispensedPrescription marked dispensed
appointment.scheduledNew appointment booked
appointment.cancelledAppointment cancelled
appointment.completedAppointment marked complete
invoice.createdNew invoice generated
invoice.paidInvoice payment received
report.readyAsync report completed

Update Events Include Previous Values

For .updated events, include changed fields:

{
  "id": "evt_def456ghi789",
  "type": "patient.updated",
  "api_version": "2.0",
  "created_at": "2024-01-07T10:35:00Z",
  "data": {
    "object": {
      "id": "pat_789",
      "full_name": "Jane Smith-Johnson",
      "email": "jane.johnson@example.com",
      "phone": "+91 98765 43210",
      "updated_at": "2024-01-07T10:35:00Z"
    },
    "previous_attributes": {
      "full_name": "Jane Smith",
      "email": "jane@example.com"
    }
  },
  "facility_id": "fac_456",
  "request_id": "req_def456"
}

Webhook Signature

Sign all webhook payloads using HMAC-SHA256:

Headers:

X-Kulcare-Signature: sha256=abc123def456...
X-Kulcare-Timestamp: 1704625800
X-Kulcare-Event: patient.created
X-Kulcare-Delivery-Id: dlv_abc123

Signature Verification (receiver side):

def verify_webhook_signature(payload, signature, timestamp, secret)
  # Prevent replay attacks - reject if older than 5 minutes
  return false if Time.now.to_i - timestamp.to_i > 300
  
  signed_payload = "#{timestamp}.#{payload}"
  expected_signature = "sha256=#{OpenSSL::HMAC.hexdigest('sha256', secret, signed_payload)}"
  
  ActiveSupport::SecurityUtils.secure_compare(expected_signature, signature)
end

Webhook Delivery Attempts

Retry failed deliveries with exponential backoff:

AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
68 hours
724 hours

After 7 failed attempts, mark webhook as failing and notify via email.

Webhook Delivery Response

Your endpoint should return HTTP 2xx within 30 seconds. Any other response triggers retry.

Successful receipt:

HTTP/1.1 200 OK

Body content is ignored.

Webhook Delivery Logs

Request: GET /webhooks/whk_789/deliveries

{
  "data": [
    {
      "id": "dlv_abc123",
      "event_id": "evt_abc123def456",
      "event_type": "patient.created",
      "status": "delivered",
      "response_code": 200,
      "response_time_ms": 245,
      "attempted_at": "2024-01-07T10:30:01Z",
      "delivered_at": "2024-01-07T10:30:01Z"
    },
    {
      "id": "dlv_def456",
      "event_id": "evt_def456ghi789",
      "event_type": "patient.updated",
      "status": "failed",
      "response_code": 500,
      "response_time_ms": 1523,
      "error_message": "Internal Server Error",
      "attempted_at": "2024-01-07T10:35:02Z",
      "next_retry_at": "2024-01-07T10:36:02Z",
      "retry_count": 1
    }
  ],
  "meta": {
    "request_id": "req_abc123",
    "pagination": {
      "total_entries": 156,
      "total_pages": 7,
      "current_page": 1,
      "per_page": 25
    }
  }
}

Retry a Failed Delivery

Request: POST /webhooks/deliveries/dlv_def456/retry

Response: HTTP 202

{
  "data": {
    "id": "dlv_def456",
    "status": "pending",
    "retry_scheduled_at": "2024-01-07T10:40:00Z"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Test Webhook

Request: POST /webhooks/whk_789/test

{
  "data": {
    "event_type": "patient.created"
  }
}

Response: HTTP 200

{
  "data": {
    "delivery_id": "dlv_test_abc123",
    "status": "delivered",
    "response_code": 200,
    "response_time_ms": 189,
    "response_body": "OK"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Webhook Status Endpoint

Provide endpoint for receivers to verify webhook health:

Request: GET /webhooks/whk_789/status

{
  "data": {
    "id": "whk_789",
    "url": "https://myapp.com/webhooks/kulcare",
    "active": true,
    "health": "healthy",
    "success_rate_7d": 98.5,
    "total_deliveries_7d": 1523,
    "failed_deliveries_7d": 23,
    "average_response_time_ms": 234,
    "last_delivery_at": "2024-01-07T10:30:01Z",
    "last_success_at": "2024-01-07T10:30:01Z",
    "last_failure_at": "2024-01-06T15:22:00Z"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Webhook Rules

  1. Sign all payloads — HMAC-SHA256 with shared secret

  2. Include timestamp in signature — Prevents replay attacks

  3. Retry with exponential backoff — Up to 7 attempts over 24 hours

  4. 30 second timeout — Fail fast, retry later

  5. Idempotency via event ID — Receivers should deduplicate by id

  6. Include API version — Receivers know payload format

  7. Log all delivery attempts — Debugging and compliance

  8. Disable after repeated failures — Notify admin, require manual re-enable

  9. Support event filtering — Don't send events receiver doesn't want

  10. Provide test endpoint — Easy integration testing


Health Check Endpoints

Basic Health Check

Request: GET /health

No authentication required.

Response (Healthy): HTTP 200

{
  "data": {
    "status": "healthy",
    "timestamp": "2024-01-07T10:30:00Z"
  },
  "meta": {}
}

Response (Unhealthy): HTTP 503

{
  "data": {
    "status": "unhealthy",
    "timestamp": "2024-01-07T10:30:00Z"
  },
  "meta": {}
}

Detailed Health Check

Request: GET /health/detailed

Requires authentication (admin or monitoring service).

Response (All Healthy): HTTP 200

{
  "data": {
    "status": "healthy",
    "version": "2.3.1",
    "environment": "production",
    "timestamp": "2024-01-07T10:30:00Z",
    "uptime_seconds": 864000,
    "checks": {
      "database": {
        "status": "healthy",
        "latency_ms": 2,
        "details": {
          "pool_size": 25,
          "active_connections": 8,
          "waiting_connections": 0
        }
      },
      "redis": {
        "status": "healthy",
        "latency_ms": 1,
        "details": {
          "connected_clients": 15,
          "used_memory_mb": 128
        }
      },
      "elasticsearch": {
        "status": "healthy",
        "latency_ms": 5,
        "details": {
          "cluster_status": "green",
          "number_of_nodes": 3
        }
      },
      "sidekiq": {
        "status": "healthy",
        "details": {
          "processes": 4,
          "queued_jobs": 23,
          "retry_jobs": 2,
          "dead_jobs": 0,
          "scheduled_jobs": 156
        }
      },
      "storage": {
        "status": "healthy",
        "latency_ms": 45,
        "details": {
          "provider": "s3",
          "region": "ap-south-1"
        }
      },
      "external_services": {
        "drug_database": {
          "status": "healthy",
          "latency_ms": 120
        },
        "sms_provider": {
          "status": "healthy",
          "latency_ms": 89
        },
        "email_provider": {
          "status": "degraded",
          "latency_ms": 2500,
          "message": "High latency detected"
        }
      }
    }
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Response (Degraded): HTTP 200

{
  "data": {
    "status": "degraded",
    "version": "2.3.1",
    "environment": "production",
    "timestamp": "2024-01-07T10:30:00Z",
    "uptime_seconds": 864000,
    "checks": {
      "database": {
        "status": "healthy",
        "latency_ms": 2
      },
      "redis": {
        "status": "healthy",
        "latency_ms": 1
      },
      "elasticsearch": {
        "status": "degraded",
        "latency_ms": 850,
        "message": "High latency - cluster rebalancing"
      },
      "sidekiq": {
        "status": "degraded",
        "message": "High queue depth",
        "details": {
          "processes": 4,
          "queued_jobs": 15000,
          "retry_jobs": 500
        }
      }
    }
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Response (Unhealthy): HTTP 503

{
  "data": {
    "status": "unhealthy",
    "version": "2.3.1",
    "environment": "production",
    "timestamp": "2024-01-07T10:30:00Z",
    "uptime_seconds": 864000,
    "checks": {
      "database": {
        "status": "unhealthy",
        "message": "Connection refused",
        "error": "PG::ConnectionBad: could not connect to server"
      },
      "redis": {
        "status": "healthy",
        "latency_ms": 1
      },
      "elasticsearch": {
        "status": "healthy",
        "latency_ms": 5
      }
    }
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Liveness Probe

For Kubernetes/container orchestration. Indicates if the process is running.

Request: GET /health/live

Response: HTTP 200

{
  "data": {
    "status": "alive",
    "timestamp": "2024-01-07T10:30:00Z"
  },
  "meta": {}
}

Readiness Probe

Indicates if the service is ready to accept traffic.

Request: GET /health/ready

Response (Ready): HTTP 200

{
  "data": {
    "status": "ready",
    "timestamp": "2024-01-07T10:30:00Z"
  },
  "meta": {}
}

Response (Not Ready): HTTP 503

{
  "data": {
    "status": "not_ready",
    "reason": "Database migrations pending",
    "timestamp": "2024-01-07T10:30:00Z"
  },
  "meta": {}
}

Version Endpoint

Request: GET /version

Response: HTTP 200

{
  "data": {
    "version": "2.3.1",
    "git_sha": "abc123def456",
    "build_time": "2024-01-05T14:30:00Z",
    "api_version": "v2",
    "environment": "production"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Status Page Data

For public status page integration:

Request: GET /health/status

No authentication required. Returns minimal, safe-to-expose information.

Response: HTTP 200

{
  "data": {
    "status": "operational",
    "components": {
      "api": "operational",
      "web_app": "operational",
      "patient_portal": "operational",
      "integrations": "degraded"
    },
    "incidents": [
      {
        "id": "inc_abc123",
        "title": "Elevated API latency",
        "status": "investigating",
        "impact": "minor",
        "created_at": "2024-01-07T10:15:00Z",
        "updated_at": "2024-01-07T10:25:00Z"
      }
    ],
    "scheduled_maintenance": [
      {
        "id": "mnt_def456",
        "title": "Database upgrade",
        "status": "scheduled",
        "scheduled_for": "2024-01-10T02:00:00Z",
        "expected_duration_minutes": 30
      }
    ]
  },
  "meta": {}
}

Health Check Status Values

StatusHTTP CodeMeaning
healthy200All systems operational
degraded200Some issues but functional
unhealthy503Critical systems failing
alive200Process is running (liveness)
ready200Accepting traffic (readiness)
not_ready503Not accepting traffic

Health Check Rules

  1. /health requires no auth — Load balancers need access

  2. /health/detailed requires auth — Exposes internal details

  3. Liveness checks should be fast — No external dependencies

  4. Readiness checks verify dependencies — Database, cache, etc.

  5. Set appropriate timeouts — Health checks should respond in < 5 seconds

  6. Cache health check results — Avoid overloading dependencies (cache for 5-10 seconds)

  7. Include version information — Helps debugging deployments

  8. Return 503 for critical failures — Load balancers can route around

  9. Return 200 for degraded state — Service still usable

  10. Log health check failures — Alert on repeated failures


Nested Resources & Relationships

Shallow Nesting (Default)

For belongs_to relationships, embed the foreign key and optionally a minimal reference:

{
  "data": {
    "id": "enc_123",
    "facility_id": "fac_456",
    "patient_id": "pat_789",
    "encounter_type": "consultation",
    "started_at": "2024-01-07T10:30:00Z"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Use the include query parameter to request related resources. Related data goes in a top-level included object, keyed by resource type.

Request: GET /encounters/enc_123?include=patient,facility,provider

Response:

{
  "data": {
    "id": "enc_123",
    "facility_id": "fac_456",
    "patient_id": "pat_789",
    "provider_id": "prv_012",
    "encounter_type": "consultation",
    "started_at": "2024-01-07T10:30:00Z"
  },
  "included": {
    "patient": {
      "id": "pat_789",
      "full_name": "Jane Smith",
      "date_of_birth": "1985-03-15"
    },
    "facility": {
      "id": "fac_456",
      "name": "Downtown Clinic",
      "facility_type": "clinic"
    },
    "provider": {
      "id": "prv_012",
      "full_name": "Dr. Raj Patel",
      "specialty": "general_medicine"
    }
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

For has_many relationships, the included value is an array:

Request: GET /patients/pat_789?include=encounters,prescriptions

{
  "data": {
    "id": "pat_789",
    "full_name": "Jane Smith",
    "date_of_birth": "1985-03-15"
  },
  "included": {
    "encounters": [
      { "id": "enc_123", "encounter_type": "consultation", "started_at": "2024-01-07T10:30:00Z" },
      { "id": "enc_124", "encounter_type": "follow_up", "started_at": "2024-01-14T11:00:00Z" }
    ],
    "prescriptions": [
      { "id": "rx_001", "medication_name": "Metformin", "status": "active" }
    ]
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Include Rules

  1. Comma-separated list — ?include=patient,facility,provider

  2. Dot notation for nested includes — ?include=encounter.facility (include encounter and its facility)

  3. Maximum include depth is 2 — Reject requests with deeper nesting

  4. Only allow whitelisted includes per endpoint — Document allowed includes for each resource

  5. Included resources use minimal representation — Only essential fields, not full resource detail

  6. Empty relationships — Use null for empty belongs_to, [] for empty has_many

{
  "included": {
    "referrer": null,
    "attachments": []
  }
}

Filtering Conventions

Query Parameter Format

Use the filter parameter with bracket notation:

GET /encounters?filter[status]=completed&filter[facility_id]=fac_456

Supported Filter Operators

Append operator suffix to field name:

OperatorSuffixExampleSQL Equivalent
Equals(none)filter[status]=activestatus = 'active'
Not equals_notfilter[status_not]=cancelledstatus != 'cancelled'
Greater than_gtfilter[created_at_gt]=2024-01-01created_at > '2024-01-01'
Greater or equal_gtefilter[amount_gte]=100amount >= 100
Less than_ltfilter[created_at_lt]=2024-02-01created_at < '2024-02-01'
Less or equal_ltefilter[amount_lte]=500amount <= 500
In list_infilter[status_in]=active,pendingstatus IN ('active', 'pending')
Not in list_not_infilter[status_not_in]=cancelled,archivedstatus NOT IN (...)
Contains_contfilter[full_name_cont]=smithfull_name ILIKE '%smith%'
Starts with_startfilter[email_start]=adminemail ILIKE 'admin%'
Is null_nullfilter[deleted_at_null]=truedeleted_at IS NULL

Filter Examples

# All active encounters at a specific facility
GET /encounters?filter[status]=active&filter[facility_id]=fac_456

# Encounters created in January 2024
GET /encounters?filter[created_at_gte]=2024-01-01T00:00:00Z&filter[created_at_lt]=2024-02-01T00:00:00Z

# Patients with name containing "patel"
GET /patients?filter[full_name_cont]=patel

# Prescriptions that are active or pending
GET /prescriptions?filter[status_in]=active,pending

# Encounters without a discharge date
GET /encounters?filter[discharged_at_null]=true

Sorting

Use the sort parameter with comma-separated fields. Prefix with - for descending:

# Sort by created_at descending (newest first)
GET /encounters?sort=-created_at

# Sort by status ascending, then created_at descending
GET /encounters?sort=status,-created_at

Sparse Fieldsets

Use the fields parameter to request specific fields only:

GET /patients?fields=id,full_name,date_of_birth

Response includes only requested fields (plus id always):

{
  "data": [
    { "id": "pat_789", "full_name": "Jane Smith", "date_of_birth": "1985-03-15" }
  ],
  "meta": { ... }
}

Pagination

Use page and per_page parameters:

GET /encounters?page=2&per_page=25

Defaults:

  • page: 1
  • per_page: 25
  • per_page maximum: 100

Combined Example

GET /encounters?filter[facility_id]=fac_456&filter[status_in]=active,completed&filter[created_at_gte]=2024-01-01&sort=-created_at&include=patient,provider&fields=id,encounter_type,started_at&page=1&per_page=50

Search Conventions

Use the q parameter for full-text search across default searchable fields:

GET /patients?q=smith

Combine q with filters to narrow search scope:

GET /patients?q=diabetes&filter[facility_id]=fac_456

Search Response

Include search metadata in the response:

{
  "data": [
    {
      "id": "pat_789",
      "full_name": "Jane Smith",
      "highlights": {
        "chief_complaint": "Patient reports <em>diabetes</em> symptoms"
      }
    }
  ],
  "meta": {
    "request_id": "req_abc123",
    "search": {
      "query": "diabetes",
      "total_results": 42,
      "searched_fields": ["full_name", "chief_complaint", "diagnosis_notes"]
    },
    "pagination": {
      "total_entries": 42,
      "total_pages": 2,
      "current_page": 1,
      "per_page": 25
    }
  }
}

Search Rules

  1. q parameter for full-text search — Searches across predefined searchable fields

  2. Document searchable fields per resource — Clients should know which fields are searched

  3. highlights object is optional — Include only when search matches occur in text fields

  4. Use <em> tags for highlight markers — Consistent with Elasticsearch conventions

  5. Combine with filters and sorting — Search respects all other query parameters

  6. Minimum query length: 2 characters — Reject single-character searches

  7. Search is case-insensitive — Always

Advanced Search (Optional)

For complex search requirements, support field-specific search:

GET /patients?search[full_name]=smith&search[diagnosis]=diabetes

Bulk Operations

Bulk Create

Request: POST /patients/bulk

{
  "data": [
    { "full_name": "John Doe", "email": "john@example.com" },
    { "full_name": "Jane Smith", "email": "jane@example.com" },
    { "full_name": "Invalid", "email": "not-an-email" }
  ]
}

Response: HTTP 207 Multi-Status

{
  "data": {
    "results": [
      {
        "index": 0,
        "status": "created",
        "data": { "id": "pat_001", "full_name": "John Doe", "email": "john@example.com" }
      },
      {
        "index": 1,
        "status": "created",
        "data": { "id": "pat_002", "full_name": "Jane Smith", "email": "jane@example.com" }
      },
      {
        "index": 2,
        "status": "failed",
        "error": {
          "code": "validation_failed",
          "message": "Validation failed",
          "details": [{ "field": "email", "message": "is invalid" }]
        }
      }
    ],
    "summary": {
      "total": 3,
      "created": 2,
      "failed": 1
    }
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Bulk Update

Request: PATCH /patients/bulk

{
  "data": [
    { "id": "pat_001", "status": "active" },
    { "id": "pat_002", "status": "active" },
    { "id": "pat_999", "status": "active" }
  ]
}

Response: HTTP 207 Multi-Status

{
  "data": {
    "results": [
      { "index": 0, "id": "pat_001", "status": "updated" },
      { "index": 1, "id": "pat_002", "status": "updated" },
      {
        "index": 2,
        "id": "pat_999",
        "status": "failed",
        "error": { "code": "not_found", "message": "Patient not found" }
      }
    ],
    "summary": {
      "total": 3,
      "updated": 2,
      "failed": 1
    }
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Bulk Delete

Request: DELETE /patients/bulk

{
  "data": {
    "ids": ["pat_001", "pat_002", "pat_003"]
  }
}

Response: HTTP 207 Multi-Status

{
  "data": {
    "results": [
      { "index": 0, "id": "pat_001", "status": "deleted" },
      { "index": 1, "id": "pat_002", "status": "deleted" },
      {
        "index": 2,
        "id": "pat_003",
        "status": "failed",
        "error": { "code": "conflict", "message": "Patient has active encounters" }
      }
    ],
    "summary": {
      "total": 3,
      "deleted": 2,
      "failed": 1
    }
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Bulk Operation Rules

  1. Always return HTTP 207 Multi-Status — Even if all operations succeed or fail

  2. Include index in each result — Maps response back to request array position

  3. Process all items — Don't stop on first failure; report all results

  4. Include summary object — Quick overview without parsing all results

  5. Maximum batch size: 100 items — Reject larger batches with 413 Payload Too Large

  6. Bulk endpoints use /resource/bulk path — Clear distinction from single-resource endpoints

  7. Atomic option — Support ?atomic=true to rollback all on any failure

POST /patients/bulk?atomic=true

With atomic=true, return standard error response on any failure:

{
  "data": null,
  "error": {
    "code": "bulk_operation_failed",
    "message": "Bulk operation rolled back due to failures",
    "details": [
      { "index": 2, "field": "email", "message": "is invalid" }
    ]
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Async Job Responses

Initiating an Async Job

For long-running operations, return HTTP 202 Accepted with job info:

Request: POST /reports/generate

{
  "data": {
    "report_type": "patient_summary",
    "facility_id": "fac_456",
    "date_range": {
      "start": "2024-01-01",
      "end": "2024-01-31"
    }
  }
}

Response: HTTP 202 Accepted

{
  "data": {
    "job_id": "job_abc123",
    "status": "pending",
    "created_at": "2024-01-07T10:30:00Z",
    "estimated_completion": "2024-01-07T10:32:00Z",
    "poll_url": "/jobs/job_abc123",
    "webhook_url": null
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Polling Job Status

Request: GET /jobs/job_abc123

Response (In Progress): HTTP 200

{
  "data": {
    "job_id": "job_abc123",
    "status": "processing",
    "progress": {
      "current": 450,
      "total": 1000,
      "percentage": 45
    },
    "created_at": "2024-01-07T10:30:00Z",
    "started_at": "2024-01-07T10:30:05Z",
    "estimated_completion": "2024-01-07T10:32:00Z"
  },
  "meta": {
    "request_id": "req_def456"
  }
}

Response (Completed): HTTP 200

{
  "data": {
    "job_id": "job_abc123",
    "status": "completed",
    "progress": {
      "current": 1000,
      "total": 1000,
      "percentage": 100
    },
    "created_at": "2024-01-07T10:30:00Z",
    "started_at": "2024-01-07T10:30:05Z",
    "completed_at": "2024-01-07T10:31:45Z",
    "result": {
      "download_url": "/reports/rpt_xyz789/download",
      "expires_at": "2024-01-08T10:31:45Z",
      "file_size": 245678,
      "file_type": "application/pdf"
    }
  },
  "meta": {
    "request_id": "req_ghi789"
  }
}

Response (Failed): HTTP 200

{
  "data": {
    "job_id": "job_abc123",
    "status": "failed",
    "created_at": "2024-01-07T10:30:00Z",
    "started_at": "2024-01-07T10:30:05Z",
    "failed_at": "2024-01-07T10:30:15Z",
    "error": {
      "code": "generation_failed",
      "message": "Report generation failed due to missing data",
      "details": [
        { "field": "facility_id", "message": "Facility has no patients in date range" }
      ]
    }
  },
  "meta": {
    "request_id": "req_jkl012"
  }
}

Job Status Values

  • pending — Job created, waiting to start
  • processing — Job is actively running
  • completed — Job finished successfully
  • failed — Job failed with error
  • cancelled — Job was cancelled by user

Cancelling a Job

Request: POST /jobs/job_abc123/cancel

Response: HTTP 200

{
  "data": {
    "job_id": "job_abc123",
    "status": "cancelled",
    "cancelled_at": "2024-01-07T10:31:00Z"
  },
  "meta": {
    "request_id": "req_mno345"
  }
}

Webhook Notifications (Optional)

If client provides a webhook URL when creating the job:

Request: POST /reports/generate

{
  "data": {
    "report_type": "patient_summary",
    "facility_id": "fac_456",
    "webhook_url": "https://myapp.com/webhooks/job-complete"
  }
}

Webhook Payload (on completion):

{
  "event": "job.completed",
  "job_id": "job_abc123",
  "status": "completed",
  "completed_at": "2024-01-07T10:31:45Z",
  "result": {
    "download_url": "https://api.kulcare.com/reports/rpt_xyz789/download",
    "expires_at": "2024-01-08T10:31:45Z"
  }
}

Async Job Rules

  1. Return HTTP 202 for accepted async jobs — Not 200 or 201

  2. Always include poll_url — Clients need a clear path to check status

  3. Include estimated_completion when possible — Helps clients decide polling frequency

  4. progress object is optional — Include only when progress is trackable

  5. Job status endpoint always returns HTTP 200 — Even for failed jobs; the status field indicates outcome

  6. Include error details in failed jobs — Same structure as standard error responses

  7. Result URLs should be time-limited — Include expires_at for download links

  8. Job results are retained for 24 hours — Document retention policy

  9. Support webhook callbacks — For clients that prefer push over poll

  10. Idempotency key for job creation — Support Idempotency-Key header to prevent duplicate jobs

POST /reports/generate
Idempotency-Key: user_report_20240107_fac456

API Versioning

URL Path Versioning

Use version prefix in URL path:

GET /v2/patients/pat_789
GET /v2/encounters?filter[status]=active

Version Rules

  1. Major version in URL — /v1/, /v2/, etc.

  2. No version means latest stable — /patients redirects to current version (not recommended for production clients)

  3. Version is required for all production integrations — Enforce via documentation and API keys

  4. Maintain previous version for minimum 12 months after deprecation announcement

  5. Use Api-Version header for minor/patch information:

Api-Version: 2.3.1

Deprecation Headers

When a version or endpoint is deprecated, include headers:

Deprecation: true
Sunset: Sat, 01 Jan 2026 00:00:00 GMT
Link: <https://api.kulcare.com/v3/patients>; rel="successor-version"

Version Response

Include version info in meta:

{
  "data": { ... },
  "meta": {
    "request_id": "req_abc123",
    "api_version": "2.3.1"
  }
}

Breaking vs Non-Breaking Changes

Non-breaking (no version bump required):

  • Adding new optional fields to responses
  • Adding new optional query parameters
  • Adding new endpoints
  • Adding new error codes

Breaking (requires version bump):

  • Removing or renaming fields
  • Changing field types
  • Removing endpoints
  • Changing authentication requirements
  • Changing default behavior

Rate Limiting

Rate Limit Headers

Include in every response:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 998
X-RateLimit-Reset: 1704628800
X-RateLimit-Window: 3600
HeaderDescription
X-RateLimit-LimitMaximum requests allowed in window
X-RateLimit-RemainingRequests remaining in current window
X-RateLimit-ResetUnix timestamp when window resets
X-RateLimit-WindowWindow duration in seconds

Rate Limit Response

When limit exceeded, return HTTP 429:

{
  "data": null,
  "error": {
    "code": "rate_limited",
    "message": "Rate limit exceeded. Try again in 342 seconds.",
    "details": [
      { "field": "retry_after", "message": "342" }
    ]
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Include Retry-After header:

Retry-After: 342

Rate Limit Tiers

Define different limits by endpoint type:

TierLimitWindowEndpoints
Standard10001 hourMost endpoints
Search1001 minute/*/search, ?q= queries
Bulk501 hour/*/bulk endpoints
Auth2015 minutes/auth/* endpoints
Reports101 hour/reports/generate

Rate Limit Rules

  1. Always include rate limit headers — Even when not rate limited

  2. Rate limit by API key, not IP — Supports legitimate multi-server deployments

  3. Separate limits for different endpoint tiers — Bulk operations more restrictive

  4. Include Retry-After on 429 responses — Both header and in error details

  5. Log rate limit hits — Monitor for abuse and adjust limits

  6. Provide rate limit status endpoint:

GET /rate_limit
{
  "data": {
    "limits": {
      "standard": { "limit": 1000, "remaining": 998, "reset": 1704628800 },
      "search": { "limit": 100, "remaining": 95, "reset": 1704625260 },
      "bulk": { "limit": 50, "remaining": 50, "reset": 1704628800 }
    }
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

File Upload Conventions

Direct Upload (Small Files < 10MB)

Request: POST /attachments

Content-Type: multipart/form-data
------WebKitFormBoundary
Content-Disposition: form-data; name="file"; filename="lab_report.pdf"
Content-Type: application/pdf

<binary data>
------WebKitFormBoundary
Content-Disposition: form-data; name="encounter_id"

enc_123
------WebKitFormBoundary
Content-Disposition: form-data; name="attachment_type"

lab_result
------WebKitFormBoundary--

Response: HTTP 201 Created

{
  "data": {
    "id": "att_789",
    "filename": "lab_report.pdf",
    "content_type": "application/pdf",
    "file_size": 245678,
    "encounter_id": "enc_123",
    "attachment_type": "lab_result",
    "download_url": "/attachments/att_789/download",
    "created_at": "2024-01-07T10:30:00Z"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Presigned Upload (Large Files > 10MB)

Step 1: Request Upload URL

Request: POST /attachments/presign

{
  "data": {
    "filename": "mri_scan.dcm",
    "content_type": "application/dicom",
    "file_size": 52428800,
    "encounter_id": "enc_123",
    "attachment_type": "imaging"
  }
}

Response: HTTP 200

{
  "data": {
    "upload_id": "upl_abc123",
    "upload_url": "https://storage.kulcare.com/uploads/upl_abc123?signature=...",
    "upload_method": "PUT",
    "upload_headers": {
      "Content-Type": "application/dicom",
      "x-amz-meta-upload-id": "upl_abc123"
    },
    "expires_at": "2024-01-07T11:30:00Z"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Step 2: Upload to Presigned URL

Client uploads directly to storage:

PUT https://storage.kulcare.com/uploads/upl_abc123?signature=...
Content-Type: application/dicom
x-amz-meta-upload-id: upl_abc123

<binary data>

Step 3: Confirm Upload

Request: POST /attachments/presign/upl_abc123/complete

{
  "data": {
    "etag": "\"d41d8cd98f00b204e9800998ecf8427e\""
  }
}

Response: HTTP 201 Created

{
  "data": {
    "id": "att_790",
    "filename": "mri_scan.dcm",
    "content_type": "application/dicom",
    "file_size": 52428800,
    "encounter_id": "enc_123",
    "attachment_type": "imaging",
    "download_url": "/attachments/att_790/download",
    "created_at": "2024-01-07T10:35:00Z"
  },
  "meta": {
    "request_id": "req_def456"
  }
}

Multipart Upload (Very Large Files > 100MB)

Step 1: Initiate Multipart Upload

Request: POST /attachments/multipart

{
  "data": {
    "filename": "ct_scan_series.zip",
    "content_type": "application/zip",
    "file_size": 524288000,
    "encounter_id": "enc_123",
    "attachment_type": "imaging"
  }
}

Response: HTTP 200

{
  "data": {
    "upload_id": "mpu_xyz789",
    "part_size": 10485760,
    "total_parts": 50,
    "parts": [
      {
        "part_number": 1,
        "upload_url": "https://storage.kulcare.com/uploads/mpu_xyz789/1?signature=...",
        "expires_at": "2024-01-07T11:30:00Z"
      },
      {
        "part_number": 2,
        "upload_url": "https://storage.kulcare.com/uploads/mpu_xyz789/2?signature=...",
        "expires_at": "2024-01-07T11:30:00Z"
      }
    ],
    "next_parts_url": "/attachments/multipart/mpu_xyz789/parts?from=3"
  },
  "meta": {
    "request_id": "req_abc123"
  }
}

Step 2: Upload Parts

Client uploads each part and collects ETags:

PUT https://storage.kulcare.com/uploads/mpu_xyz789/1?signature=...
Content-Length: 10485760

<binary data for part 1>

Response header: ETag: "abc123..."

Step 3: Complete Multipart Upload

Request: POST /attachments/multipart/mpu_xyz789/complete

{
  "data": {
    "parts": [
      { "part_number": 1, "etag": "\"abc123...\"" },
      { "part_number": 2, "etag": "\"def456...\"" },
      { "part_number": 3, "etag": "\"ghi789...\"" }
    ]
  }
}

Response: HTTP 201 Created

{
  "data": {
    "id": "att_791",
    "filename": "ct_scan_series.zip",
    "content_type": "application/zip",
    "file_size": 524288000,
    "encounter_id": "enc_123",
    "attachment_type": "imaging",
    "download_url": "/attachments/att_791/download",
    "created_at": "2024-01-07T10:45:00Z"
  },
  "meta": {
    "request_id": "req_ghi789"
  }
}

File Download

Request: GET /attachments/att_789/download

Response: HTTP 302 Redirect

Location: https://storage.kulcare.com/files/att_789?signature=...&expires=1704632400

Or for inline viewing:

Request: GET /attachments/att_789/download?disposition=inline

File Upload Rules

  1. Direct upload for files < 10MB — Simpler for small files

  2. Presigned upload for 10MB - 100MB — Offloads bandwidth from API server

  3. Multipart for > 100MB — Enables resumable uploads

  4. Validate file types — Whitelist allowed MIME types per attachment_type

  5. Scan for malware — Queue uploaded files for scanning before making available

  6. Generate thumbnails for images — Include thumbnail_url in response

  7. Time-limited download URLs — 1 hour expiry default

  8. Include file metadata in response:

{
  "data": {
    "id": "att_789",
    "filename": "lab_report.pdf",
    "content_type": "application/pdf",
    "file_size": 245678,
    "checksum": {
      "algorithm": "sha256",
      "value": "abc123def456..."
    },
    "dimensions": null,
    "thumbnail_url": null,
    "download_url": "/attachments/att_789/download",
    "created_at": "2024-01-07T10:30:00Z"
  }
}
  1. Support resumable downloads:
GET /attachments/att_789/download
Range: bytes=1000-1999

Response: HTTP 206 Partial Content

Content-Range: bytes 1000-1999/245678

Allowed File Types by Category

CategoryAllowed MIME TypesMax Size
documentapplication/pdf, application/msword, application/vnd.openxmlformats-*25MB
imageimage/jpeg, image/png, image/gif, image/webp10MB
lab_resultapplication/pdf, image/jpeg, image/png25MB
imagingapplication/dicom, application/zip500MB
audioaudio/mpeg, audio/wav, audio/ogg100MB
videovideo/mp4, video/webm500MB

Implementation Notes

  1. Create a base response builder/serializer — Enforces envelope structure across all endpoints

  2. Whitelist filters per resource — Never allow arbitrary filter fields; explicitly declare what's filterable

  3. Whitelist sortable fields — Prevent sorting on unindexed columns

  4. Whitelist includable relationships — Document and enforce per endpoint

  5. Validate filter values — Type-check and sanitize all filter inputs

  6. Generate request_id for every request — Log it server-side, return it to client

  7. Never return raw ActiveRecord errors — Transform to consistent error structure

  8. Empty collections return [] — Never null for collections

  9. Index your filter columns — Any field exposed for filtering should have a database index

  10. Consider caching includes separately — Included resources can be cached independently

  11. Use background job library — Sidekiq or similar for async operations

  12. Store job state in Redis or database — Jobs must survive server restarts

  13. Implement job cleanup — Purge old job records and results

  14. Rate limit bulk endpoints separately — More restrictive than single-resource endpoints

  15. Log all bulk operations — Audit trail for batch changes

  16. Use cloud storage for file uploads — S3, GCS, or Azure Blob with presigned URLs

  17. Implement virus scanning — ClamAV or cloud-based scanning for all uploads

  18. Version your API from day one — Easier than retrofitting later

  19. Monitor rate limit hits — Adjust limits based on real usage patterns

  20. Document everything — Each endpoint should list allowed filters, includes, sort fields

  21. Use secure token storage — Never store plaintext tokens; use hashed values

  22. Implement token rotation — Refresh tokens should be single-use

  23. Log all authentication events — Success, failure, logout for audit trail

  24. Sign all webhook payloads — HMAC-SHA256 with timestamp

  25. Implement webhook retry with backoff — Exponential backoff up to 24 hours

  26. Health checks should be lightweight — Don't overload dependencies

  27. Separate liveness from readiness — Different purposes for orchestration

On this page

Core EnvelopeRulesSuccess Response (Single Resource)Success Response (Collection)Error ResponseError CodesAuthentication & AuthorizationAuthentication MethodsBearer Token AuthenticationAPI Key AuthenticationLogin FlowToken RefreshLogoutAuthentication ErrorsMulti-Factor AuthenticationAuthorization HeadersPermission DeniedResource-Level AuthorizationCurrent User EndpointSwitch Facility ContextAPI Key ManagementAuthentication RulesWebhook DeliveryWebhook RegistrationWebhook Payload StructureEvent TypesUpdate Events Include Previous ValuesWebhook SignatureWebhook Delivery AttemptsWebhook Delivery ResponseWebhook Delivery LogsRetry a Failed DeliveryTest WebhookWebhook Status EndpointWebhook RulesHealth Check EndpointsBasic Health CheckDetailed Health CheckLiveness ProbeReadiness ProbeVersion EndpointStatus Page DataHealth Check Status ValuesHealth Check RulesNested Resources & RelationshipsShallow Nesting (Default)Including Related ResourcesIncluding Related CollectionsInclude RulesFiltering ConventionsQuery Parameter FormatSupported Filter OperatorsFilter ExamplesSortingSparse FieldsetsPaginationCombined ExampleSearch ConventionsBasic SearchScoped SearchSearch ResponseSearch RulesAdvanced Search (Optional)Bulk OperationsBulk CreateBulk UpdateBulk DeleteBulk Operation RulesAsync Job ResponsesInitiating an Async JobPolling Job StatusJob Status ValuesCancelling a JobWebhook Notifications (Optional)Async Job RulesAPI VersioningURL Path VersioningVersion RulesDeprecation HeadersVersion ResponseBreaking vs Non-Breaking ChangesRate LimitingRate Limit HeadersRate Limit ResponseRate Limit TiersRate Limit RulesFile Upload ConventionsDirect Upload (Small Files < 10MB)Presigned Upload (Large Files > 10MB)Step 1: Request Upload URLStep 2: Upload to Presigned URLStep 3: Confirm UploadMultipart Upload (Very Large Files > 100MB)Step 1: Initiate Multipart UploadStep 2: Upload PartsStep 3: Complete Multipart UploadFile DownloadFile Upload RulesAllowed File Types by CategoryImplementation Notes