{
  "version": "1",
  "upload": {
    "max_size_bytes": 26214400,
    "global_ceiling_bytes": 52428800,
    "supported_mime_types": [
      "application/json",
      "application/octet-stream",
      "application/pdf",
      "application/xml",
      "application/zip",
      "image/gif",
      "image/jpeg",
      "image/png",
      "image/webp",
      "text/csv",
      "text/html",
      "text/plain"
    ],
    "content_field_name": "content",
    "accepted_content_types": [
      "multipart/form-data",
      "application/json"
    ],
    "note_json": "For application/json, the content field must be a base64-encoded string. Body limit is 1 MB.",
    "size_enforcement": "Upload size is enforced in two stages: (1) early rejection if a Content-Length header exceeds the plan limit, before the body is read; (2) mid-stream byte counting during upload, aborting and returning payload_too_large even when Content-Length is absent (chunked transfer). Plan-specific limits apply when plan enforcement is active."
  },
  "fields": {
    "mime_type": {
      "required": true,
      "max_length": 100
    },
    "label": {
      "required": false,
      "max_length": 200
    },
    "expires_at": {
      "required": false,
      "format": "ISO 8601 date-time string",
      "default_ttl_seconds": 604800,
      "max_ttl_seconds": 2592000
    },
    "public": {
      "required": false,
      "type": "boolean",
      "default": false
    },
    "metadata": {
      "required": false,
      "properties": {
        "agentId": {
          "type": "string",
          "max_length": 100
        },
        "jobId": {
          "type": "string",
          "max_length": 100
        },
        "sourceSystem": {
          "type": "string",
          "max_length": 100
        },
        "tags": {
          "type": "array",
          "max_count": 10,
          "max_item_length": 50
        },
        "custom": {
          "type": "object",
          "max_bytes": 4096,
          "max_depth": 3,
          "max_keys": 50
        }
      }
    }
  },
  "idempotency": {
    "header": "Idempotency-Key",
    "required": true,
    "required_on": "POST /v1/outputs only — not required on any other endpoint",
    "scope": "per api key + per key value",
    "ttl_seconds": 604800,
    "key_constraints": {
      "max_length": 255,
      "charset": "printable ASCII (0x20–0x7E)"
    },
    "concurrent_same_key": {
      "behavior": "If two requests arrive with the same Idempotency-Key simultaneously, only the first is processed. The second receives HTTP 503 with error code idempotency_in_flight and a Retry-After: 5 header. Retry with the identical key and payload after the specified wait.",
      "response_code": 503,
      "error_code": "idempotency_in_flight",
      "retry_after_seconds": 5
    },
    "note": "Retry a failed upload with the same Idempotency-Key to get a fresh attempt, not a cached failure."
  },
  "retrieval": {
    "modes": [
      {
        "id": "authenticated",
        "field": "contentUrl",
        "description": "Authenticated private download. Use GET /v1/outputs/{id}/content with your API key. Returns raw bytes with the stored MIME type. Content-Disposition is set to attachment; filename=\"{label}\" when label was provided at creation.",
        "auth_required": true,
        "available_when": "status: ready"
      },
      {
        "id": "public_cdn",
        "field": "publicUrl",
        "description": "CDN public URL. Available only when the output was created with public: true. Serves the file directly from the CDN edge with no authentication. Safe to embed in messages or share with end users.",
        "auth_required": false,
        "available_when": "status: ready AND public: true at creation",
        "opt_in": true,
        "note": "public: true cannot be added retroactively in V1."
      }
    ],
    "url_nulling_rules": {
      "description": "contentUrl and publicUrl are null for any status other than ready. Both fields are set to null in the DB atomically when an output expires or is deleted.",
      "statuses_with_null_urls": [
        "failed",
        "expired",
        "deleted"
      ]
    }
  },
  "account": {
    "free_tier_initial_uploads": 5,
    "free_tier_initial_storage_bytes": 262144000,
    "note": "These are the initial free-tier defaults assigned at registration. Current account state is returned in X-OutputLayer-* response headers on authenticated requests."
  },
  "auth": {
    "scheme": "Bearer",
    "header": "Authorization",
    "register_endpoint": "/v1/keys/register",
    "token_type": "Raw API key issued by POST /v1/keys/register — not an OAuth JWT. Send as: Authorization: Bearer <api_key>. OAuth Bearer token (JWT) support is reserved for a future version."
  },
  "versioning": {
    "current_version": "1",
    "supported_versions": [
      "1"
    ],
    "breaking_change_policy": "Removing or renaming fields, changing field types, removing endpoints, adding required parameters, or changing HTTP status codes for existing scenarios require a new major version (/v2/). Non-breaking additions stay in /v1/.",
    "deprecation_policy": "Deprecated endpoints coexist for a minimum of 6 months. Responses from deprecated endpoints include Deprecation and Sunset HTTP headers and an agent_contract.migration_note field with migration instructions."
  },
  "pricing_model": {
    "free_trial": {
      "uploads": 5,
      "storage_bytes": 262144000,
      "note": "Free trial is assigned at registration. No payment required."
    },
    "credit_cost_per_upload": 1,
    "deduction_order": "free_trial quota first, then paid credits",
    "packs": [
      {
        "packId": "basic",
        "label": "Basic",
        "credits": 200,
        "storage_bytes": 2147483648,
        "price_usd": "6.99"
      },
      {
        "packId": "pro",
        "label": "Pro",
        "credits": 600,
        "storage_bytes": 10737418240,
        "price_usd": "16.99"
      },
      {
        "packId": "agency",
        "label": "Agency",
        "credits": 2000,
        "storage_bytes": 53687091200,
        "price_usd": "44.99"
      }
    ],
    "notes": [
      "Credits never expire.",
      "Storage quota is pooled across the account and increases with each pack purchase.",
      "1 credit is deducted per successful upload, regardless of file size (subject to storage_bytes limit)."
    ]
  },
  "billing": {
    "checkout_endpoint": "POST /v1/credits/checkout",
    "verify_endpoint": "GET /v1/credits/verify",
    "balance_endpoint": "GET /v1/credits/balance",
    "webhook_endpoint": "POST /webhooks/paypal",
    "provider": "PayPal",
    "payment_flow": [
      "POST /v1/credits/checkout → returns purchaseId + checkoutUrl",
      "Redirect user to checkoutUrl to approve payment on PayPal",
      "PayPal fires CHECKOUT.ORDER.APPROVED → server captures funds (no credits yet)",
      "PayPal fires PAYMENT.CAPTURE.COMPLETED → server adds credits + storage atomically",
      "Poll GET /v1/credits/verify?purchaseId={purchaseId} until status is confirmed"
    ],
    "packs": [
      {
        "packId": "basic",
        "label": "Basic",
        "creditsToAdd": 200,
        "storageBytesToAdd": 2147483648,
        "priceUsd": "6.99"
      },
      {
        "packId": "pro",
        "label": "Pro",
        "creditsToAdd": 600,
        "storageBytesToAdd": 10737418240,
        "priceUsd": "16.99"
      },
      {
        "packId": "agency",
        "label": "Agency",
        "creditsToAdd": 2000,
        "storageBytesToAdd": 53687091200,
        "priceUsd": "44.99"
      }
    ]
  },
  "error_codes": [
    "missing_idempotency_key",
    "invalid_mime_type",
    "payload_too_large",
    "invalid_expiry",
    "invalid_request",
    "invalid_metadata",
    "idempotency_conflict",
    "quota_exhausted",
    "storage_limit_reached",
    "output_not_found",
    "output_expired",
    "output_deleted",
    "upload_failed",
    "missing_api_key",
    "invalid_api_key",
    "rate_limited",
    "idempotency_in_flight",
    "server_error",
    "purchase_not_found"
  ],
  "contact": {
    "support": "support@outputlayer.dev",
    "security": "security@outputlayer.dev"
  },
  "output_statuses": [
    "ready",
    "failed",
    "expired",
    "deleted"
  ],
  "listing": {
    "default_limit": 20,
    "max_limit": 100,
    "ordering": "id DESC — newest output first",
    "pagination": "cursor-based; pass pageInfo.nextCursor from the previous response as the cursor query parameter",
    "filters": {
      "limit": {
        "type": "integer",
        "min": 1,
        "max": 100,
        "default": 20
      },
      "cursor": {
        "type": "string",
        "description": "Opaque cursor from previous pageInfo.nextCursor. Not required to match a real output id."
      },
      "status": {
        "type": "string",
        "valid_values": [
          "ready",
          "failed",
          "expired",
          "deleted"
        ],
        "description": "Exact-match filter. Omit to return all statuses."
      },
      "mimeType": {
        "type": "string",
        "description": "Exact-match filter on MIME type. Omit to return all types."
      }
    }
  },
  "rate_limits": {
    "note": "All limits use rolling windows. Authenticated limits are per API key (Authorization header value). Discovery limits are per IP address.",
    "authenticated": {
      "POST /v1/outputs": {
        "limit": 60,
        "window_seconds": 3600
      },
      "GET /v1/outputs": {
        "limit": 2000,
        "window_seconds": 3600
      },
      "GET /v1/outputs/{id}": {
        "limit": 2000,
        "window_seconds": 3600
      },
      "GET /v1/outputs/{id}/content": {
        "limit": 2000,
        "window_seconds": 3600
      },
      "DELETE /v1/outputs/{id}": {
        "limit": 200,
        "window_seconds": 3600
      },
      "credits_endpoints": {
        "limit": 60,
        "window_seconds": 3600,
        "note": "/v1/credits, /v1/purchases, /v1/idempotency"
      }
    },
    "discovery": {
      "GET /v1/capabilities": {
        "limit": 60,
        "window_seconds": 60
      },
      "GET /v1/tool": {
        "limit": 60,
        "window_seconds": 60
      },
      "GET /v1/schema": {
        "limit": 60,
        "window_seconds": 60
      },
      "GET /v1/examples": {
        "limit": 60,
        "window_seconds": 60
      },
      "GET /.well-known/agent.json": {
        "limit": 60,
        "window_seconds": 60
      }
    },
    "registration": {
      "POST /v1/keys/register": {
        "limit": 10,
        "window_seconds": 3600,
        "key": "per IP"
      }
    }
  },
  "plan_tiers": {
    "free": {
      "uploads_per_hour": 10,
      "max_file_size_bytes": 5242880,
      "storage_limit_bytes": 262144000
    },
    "basic": {
      "uploads_per_hour": 60,
      "max_file_size_bytes": 10485760,
      "storage_limit_bytes": 2147483648
    },
    "pro": {
      "uploads_per_hour": 120,
      "max_file_size_bytes": 26214400,
      "storage_limit_bytes": 10737418240
    },
    "agency": {
      "uploads_per_hour": 240,
      "max_file_size_bytes": 52428800,
      "storage_limit_bytes": 53687091200
    }
  },
  "conventions": {
    "output_resource_fields": "camelCase (outputId, mimeType, sizeBytes, createdAt, expiresAt, publicUrl, contentUrl) — V1 convention; changing to snake_case is a breaking change reserved for /v2/.",
    "error_envelope_fields": "snake_case (error, message, request_id, agent_contract)",
    "agent_contract_fields": "snake_case (content_accessible, safe_to_share, expires_in_seconds, next_actions, migration_note)",
    "action_codes": "verb_noun lowercase — additive-only in /v1/; removing or renaming codes is breaking",
    "timestamps": "ISO 8601 UTC with Z suffix (toISOString())",
    "sizes": "bytes only (integer)",
    "id_format": "{prefix}_{monotonic_ulid} — out_, key_, purch_"
  }
}