# API Field Documentation ## Overview This document describes the fields in the API response, including their definitions and logic used for computation. ### How to generate an API token ```ruby my_key = ApiKey.create!(consumer_name: "Example key") token = my_key.key_string ``` ### Defined route and controller files ```ruby! namespace :v2 do resources :appeals, only: :index end ``` The most relevant controller file is: `app/controllers/api/v2/appeals_controller.rb` and the data is primarily generated via this file: `app/services/api/v2/appeals.rb` ### Serializers There are 4 relevant serializers for this API. Higher-Level Reviews: `app/models/serializers/v2/hlr_status_serializer.rb` Supplemental Claims: `app/models/serializers/v2/sc_status_serializer.rb` Appeals: `app/models/serializers/v2/appeal_status_serializer.rb` Legacy Appeals: `app/models/serializers/v2/legacy_appeal_status_serializer.rb` ### Example curl request to the api ``` curl -X GET -H "Authorization: Token dba6a974c0de4e33996a33c0eac886d0" -H "Content-Type: application/json" -H "X-VA-Receipt-Date: 2025-02-05" -H "ssn: 252866460" "http://localhost:3000/api/v2/appeals" ``` ### Example api return ```json { "data": [ { "id": "HLR80", "type": "higherLevelReview", "attributes": { "appealIds": ["HLR80"], "updated": "2025-02-06T17:01:16-05:00", "incompleteHistory": false, "active": true, "description": "Streptococcic arthritis", "location": "aoj", "aoj": "vha", "programArea": "medical", "status": { "type": "hlr_received", "details": {} }, "alerts": [], "issues": [ { "active": true, "lastAction": null, "date": null, "description": "Streptococcic arthritis", "diagnosticCode": "5008" } ], "events": [ { "type": "hlr_request", "date": "2024-12-30" } ], "evidence": [] } } ] } ``` ## Fields ### `appeal_ids` #### Attribute Description Resolved by `linked_review_ids`, which is an array containing a single string. #### Possible values `string` since it's an infinite number of integers #### Code snippets ```ruby def review_status_id "HLR#{id}" end def linked_review_ids Array.wrap(review_status_id) end ``` ### `updated` #### Attribute Description Always the current time, converted to Eastern Time and rounded into an ISO 8601 date string. #### Possible values `ISO 8601 date string` #### Code snippets ```ruby Time.zone.now.in_time_zone("Eastern Time (US & Canada)").round.iso8601 ``` ### `incomplete_history` #### Attribute Description Always `false` for non-legacy appeals. For legacy appeals it is marked as true if any of the nodes of the generated appeal series is marked as incomplete which is a node without a parent in the tree #### Possible values `boolean` : true or false #### Code snippets ```ruby # for other review types attribute :incomplete_history do false end # For legacy appeals appeal_tree_nodes.each do |node| # Set the series, joining through the merge table to the series table. appeal_series = series_table[merge_table[node[:series_id]]] appeal_series.appeals << node[:appeal] # If any node is marked as incomplete, the series is marked as incomplete. appeal_series.update(incomplete: true) if node[:incomplete] end ``` ### `active` #### Attribute Description A boolean indicating active status, determined by the `active_status?` method. Factors include benefit type, existing EPS, and active tasks associated with the review. #### Possible values `boolean` : true or false #### Code snippets ```ruby # For appeals def active_status? # For the appeal status api, and Appeal is considered open # as long as there are active remand claim or effectuation # tracked in VBMS. active? || active_effectuation_ep? || active_remanded_claims? end # for HLRs active? includes End product check as well def active_status? # for the purposes for appeal status api, an HLR is considered active if there are # still active remand claims. active? || active_remanded_claims? end # SCs same as HLRs but without the active remand check def active_status? active? end # Legacy appeal def active? # All issues on an appeal have not yet been granted or denied status != "Complete" end ``` ### `description` #### Attribute Description A string representing the number of request issues associated with a review. It can be derived from diagnostic codes, claim types, or issue count. Implementation: #### Possible values `string` : it can be a very large number of possibilities since it's issue count + benefit types and other things #### Code snippets ```ruby def description return if request_issues.empty? descripton = fetch_status_description_using_diagnostic_code return descripton if descripton description = fetch_status_description_using_claim_type return description if description return "1 issue" if request_issues.count == 1 "#{request_issues.count} issues" end def fetch_status_description_using_diagnostic_code issue = request_issues.find do |ri| !ri[:contested_rating_issue_diagnostic_code].nil? end description = issue.api_status_description if issue return unless description return description if request_issues.count - 1 == 0 return "#{description} and 1 other" if request_issues.count - 1 == 1 "#{description} and #{request_issues.count - 1} others" end def fetch_status_description_using_claim_type return if program == "other" || program == "multiple" return "1 #{program} issue" if request_issues.count == 1 "#{request_issues.count} #{program} issues" end ``` ### `aod` #### Attribute description An attribute depicting whether or not an appeal or legacy appeal has been marked as advanced on docket. It is resolved by the `advanced_on_docket?` method for appeals and the `aod` attribute on legacy appeals **Special note: This attribute is only present on Appeals and Legacy Appeals not HLRs and SCs** #### Possible values `boolean`: True or false based on the aod status #### Code snippets ```ruby # For Appeals. It is based on several different criteria often related to the claimant model def advanced_on_docket? conditionally_set_aod_based_on_age # One of the AOD motion reasons is 'age'. Keep interrogation of any motions separate from `aod_based_on_age`, # which reflects `claimant.advanced_on_docket_based_on_age?`. aod_based_on_age || claimant&.advanced_on_docket_motion_granted?(self) end #For legacy appeals it comes straight out of Vacols def aod(vacols_id) VACOLS::Case.aod([vacols_id])[vacols_id] end ``` ### `location` #### Attribute description Always `"aoj"` for SCs and HLRs. For appeals and legacy appeals, determined by active EPs, remands, or latest appeal status. Implementation: #### Possible values `enum`: the value can only be "aoj" or "bva" regardless of review type #### Code snippets ```ruby # Hlrs and SCs attribute :location do "aoj" end # Appeals def location if active_effectuation_ep? || active_remanded_claims? "aoj" else "bva" end end # Legacy appeals def location %w[Advance Remand].include?(latest_appeal.status) ? :aoj : :bva end ``` ### `aoj` #### Attribute description Derived from request issues and benefit type. For appeals, it may be "other" if issues have different benefit types. For legacy appeals, it finds the first non-nil `aoj` value from all issues or defaults to "other". #### Possible values `string` or possibly `enumeration`: The total number of values seems to be fixed based on all the possible benefit types, the list of aoj values from legacy appeals, and "other". If the legacy aoj values are non variable then it can be considered an enumeration #### Code snippets ```ruby # For HLRs and SCs def aoj return if request_issues.empty? request_issues.first.api_aoj_from_benefit_type end # For appeals def aoj return if request_issues.empty? return "other" unless all_request_issues_same_aoj? request_issues.first.api_aoj_from_benefit_type end # Legacy appeal version def aoj appeals.lazy.flat_map(&:issues).map(&:aoj).find { |aoj| !aoj.nil? } || :other end # The request issue method that decides the aoj type def api_aoj_from_benefit_type case benefit_type when "compensation", "pension", "fiduciary", "insurance", "education", "voc_rehab", "loan_guaranty" "vba" else benefit_type end end ``` ### `program` #### Attribute description Determines the benefit type for HLRs and SCs. For appeals, if all request issues share the same benefit type, that type is used; otherwise, it is "multiple". **Special note: For Appeals, HLRs, and SCs the benefit types listed below get changed to "vba" "compensation", "pension", "fiduciary", "insurance", "education", "voc_rehab", and "loan_guaranty"** #### Possible values `enumeration`: It can only be one of the following values vre, medical, burial, multiple, compensation, pension, fiduciary, insurance, education, voc_rehab, loan_guaranty, nca, vha, vba_burial, compensation, education, insurance, loan_guaranty, medical, pension, vre, other, bva, nca_burial, vba, or fiduciary #### Code snippets ```ruby # HLRs and SCs def program case benefit_type when "voc_rehab" "vre" when "vha" "medical" when "nca" "burial" else benefit_type end end #Appeal version def program return if request_issues.empty? if request_issues.all? { |ri| ri.benefit_type == request_issues.first.benefit_type } request_issues.first.benefit_type else "multiple" end end # Benefit types { "compensation": "Compensation", "pension": "Pension & Survivor's Benefits", "fiduciary": "Fiduciary", "insurance": "Insurance", "education": "Education", "voc_rehab": "Veterans Readiness and Employment", "loan_guaranty": "Loan Guaranty", "nca": "National Cemetery Administration", "vha": "Veterans Health Administration" } # For legacy appeals it is either one of these values from the constant hash below or "multiple" PROGRAMS = { "01" => :vba_burial, "02" => :compensation, "03" => :education, "04" => :insurance, "05" => :loan_guaranty, "06" => :medical, "07" => :pension, "08" => :vre, "09" => :other, "10" => :bva, "11" => :nca_burial, "12" => :fiduciary }.freeze ``` ### `status` #### Attribute description Fetched via a serializer with attributes `type` and `details`. ```ruby attribute :type, &:fetch_status attribute :details, &:fetch_details_for_status ``` ##### type attribute ##### Sub Attribute description A sub attribute of the status attribute that is aliased to the `fetch_status` method ##### Possible values `enumeration`: The list of values is a fixed number of strings but it is scattered around in 10+ methods: TODO: Build a complete list of values for appeals and legacy appeals Possible values for `fetch_status`: - HLR: `:hlr_received`, `:hlr_dta_error`, `:hlr_decision`, and `:hlr_closed` - SC: `:sc_received`, `:sc_closed`, and `:sc_decision` - Appeals: Multiple status types including `:pre_docketed`, `:decision_in_progress`, `:bva_decision`, etc. - Legacy appeals: Same as appeals it includes a large amount of status types Appeal values ```ruby :pre_docketed, :pending_hearing_scheduling, :scheduled_hearing, :evidentiary_period, :at_vso, :decision_in_progress, :on_docket, :ama_remand, :post_bva_dta_decision, :bva_decision_effectuation, :bva_decision, :decision_in_progress, :withdrawn, :other_close ``` Legacy Appeal values: ```ruby [ :scheduled_hearing, :pending_hearing_scheduling, :on_docket, :pending_certification_ssoc, :pending_certification, :pending_form9, :pending_soc, :stayed, :at_vso, :bva_development, :decision_in_progress, :bva_decision, :field_grant, :withdrawn, :ftr, :ramp, :statutory_opt_in, :death, :reconsideration, :merged, :other_close, :remand_ssoc, :remand, :motion, :cavc ] ``` ##### Code snippets ```ruby # For HLRs def fetch_status if active? :hlr_received elsif active_remanded_claims? :hlr_dta_error elsif remand_supplemental_claims.any? remand_supplemental_claims.each do |rsc| return :hlr_decision if rsc.decision_issues.any? end :hlr_closed else decision_issues.empty? ? :hlr_closed : :hlr_decision end end # For SCs def fetch_status if active? :sc_recieved else decision_issues.empty? ? :sc_closed : :sc_decision end end # For appeals and more unlisted chained method calls def fetch_status if open_pre_docket_task? :pre_docketed elsif active? fetch_pre_decision_status else fetch_post_decision_status end end # for legacy appeals and more unlisted chained method calls def fetch_status case latest_appeal.status when "Advance" disambiguate_status_advance when "Active" disambiguate_status_active when "Complete" disambiguate_status_complete when "Remand" disambiguate_status_remand when "Motion" :motion when "CAVC" :cavc end end ``` ##### description attribute ##### Sub Attribute description A sub attribute of the status attribute that is aliased to the `fetch_details_for_status` method ##### Possible values `hash` or `object` - It builds json for all the fetched decision issues on the review based on benefit type and disposition The json will often end up looking like this. There are other keys for hearings and other types of statuses sometimes ```ruby { issues: api_issues_for_status_details_issues(issue_list) } # The issues values { description: issue.api_status_description, disposition: issue.api_status_disposition } ``` ##### Code snippets ```ruby # HLRs def fetch_details_for_status case fetch_status when :hlr_decision issue_list = fetch_all_decision_issues { issues: api_issues_for_status_details_issues(issue_list) } else {} end end # SCs def fetch_details_for_status case fetch_status when :sc_decision { issues: api_issues_for_status_details_issues } else {} end end # Appeals def fetch_details_for_status # rubocop:disable Metrics/MethodLength case fetch_status when :bva_decision { issues: api_issues_for_status_details_issues(decision_issues) } when :ama_remand { issues: api_issues_for_status_details_issues(decision_issues) } when :post_bva_dta_decision post_bva_dta_decision_status_details when :bva_decision_effectuation { bva_decision_date: decision_event_date, aoj_decision_date: decision_effectuation_event_date } when :pending_hearing_scheduling { type: "video" } when :scheduled_hearing api_scheduled_hearing_status_details when :decision_in_progress { decision_timeliness: AppealSeries::DECISION_TIMELINESS.dup } else {} end end # Legacy Appeals def fetch_details_for_status case status when :scheduled_hearing hearing = latest_appeal.scheduled_hearings.min_by(&:scheduled_for) { date: hearing.scheduled_for.to_date, type: hearing.readable_request_type.downcase, location: hearing.request_type_location } when :pending_hearing_scheduling { type: latest_appeal.current_hearing_request_type } when :pending_form9, :pending_certification, :pending_certification_ssoc { last_soc_date: last_soc_date, certification_timeliness: CERTIFICATION_TIMELINESS.dup, ssoc_timeliness: SSOC_TIMELINESS.dup } when :pending_soc { soc_timeliness: SOC_TIMELINESS.dup } when :at_vso { vso_name: representative_name } when :decision_in_progress { decisionTimeliness: DECISION_TIMELINESS.dup } when :remand { issues: issues_for_last_decision, remand_timeliness: REMAND_TIMELINESS.dup } when :remand_ssoc { last_soc_date: last_soc_date, return_timeliness: RETURN_TIMELINESS.dup, remand_ssoc_timeliness: REMAND_SSOC_TIMELINESS.dup } when :bva_decision { issues: issues_for_last_decision } else {} end end # Issues get build by this. Which ends up being different based on benefit type and disposition def api_issues_for_status_details_issues(issue_list) issue_list.map do |issue| { description: issue.api_status_description, disposition: issue.api_status_disposition } end ``` ### `alerts` #### Attribute description An array of objects that varies based on the type of decision review and relevant data that is sorted by decision date #### Possible values `array`: It's an array of objects of this type of structure. The keys change based on the type of alert that is built ```ruby { type: "ama_post_decision", details: { decisionDate: decision_review.decision_effectuation_event_date, availableOptions: decision_review.available_review_options, dueDate: decision_review.decision_effectuation_event_date + 365.days, cavcDueDate: decision_review.decision_effectuation_event_date + 120.days } } ``` #### Code snippets ```ruby def alerts @alerts ||= ApiStatusAlerts.new(decision_review: self).all.sort_by { |alert| alert[:details][:decisionDate] } end # Alert types for HLRs and SCs. Only includes the post_decision alert def claim_review_alerts [ post_decision ].compact end # Appeal alerts are more varied def appeal_alerts [ post_decision, post_remand_decision, post_effectuation, evidentiary_period, scheduled_hearing ].flatten.compact.uniq end # The only kind of alerts for HLRs and SCs def post_decision return unless decision_review.api_alerts_show_decision_alert? return unless Time.zone.today <= decision_review.due_date_to_appeal_decision return if appeal? && Time.zone.today > decision_review.cavc_due_date { type: "ama_post_decision", details: { decisionDate: decision_review.decision_date_for_api_alert, availableOptions: decision_review.available_review_options, dueDate: decision_review.due_date_to_appeal_decision, cavcDueDate: appeal? ? (decision_review.decision_date_for_api_alert + 120.days) : nil } } end # Example of a different kind of alert. This one is hearing related def scheduled_hearing return unless decision_review.hearing_docket? scheduled_hearing = decision_review.scheduled_hearing return unless scheduled_hearing { type: "scheduled_hearing", details: { date: scheduled_hearing.scheduled_for.to_date, type: decision_review.api_scheduled_hearing_type } } end ``` ### `issues` #### Attribute description Uses the normal issue serializer to serialize all active request issues or decision issues on the review #### Possible values `object` or `hash`: It's an object of objects for all active issues on the review. The total possible combination of values is very large since it reuses the normal IssueSerializer The attributes on the issue serializer is listed below in the code snippets. These chain into some of the methods listed above for some of the other attribute fields as well like the `api_status_description` #### Code snippets ```ruby def issues(object) IssueSerializer.new(object.active_request_issues_or_decision_issues, is_collection: true) .serializable_hash[:data].collect { |issue| issue[:attributes] } end # Issue serializer attributes attribute :active, &:api_status_active? attribute :last_action, &:api_status_last_action attribute :date, &:api_status_last_action_date attribute :description, &:api_status_description attribute :diagnostic_code, &:diagnostic_code # Legacy appeal issues are generated by the AppealSeriesIssues rather than the Issues serializer def issues @issues ||= AppealSeriesIssues.new(appeal_series: self).all end ``` #### Last action Issue attribute The last action attribute is the disposition of the issue for HLRs, SCs, and Appeals. It is resolved by the `api_status_last_action` method for all 3 types. For request issues the return value is always nil ```ruby! def api_status_last_action # this will be nil # may need to be updated if an issue is withdrawn end ``` For decision issues the disposition is returned or "remand" if it was a "remanded disposition" ```ruby! def api_status_last_action return "remand" if disposition == "remanded" disposition end ``` The disposition list for Appeals. It comes from the `client/constants/ISSUE_DISPOSITIONS_BY_ID.json` json file ```json { "allowed": "Allowed", "remanded": "Remanded", "denied": "Denied", "vacated": "Vacated", "dismissed_death": "Dismissed, Death", "dismissed_matter_of_law": "Dismissed, Matter of Law", "withdrawn": "Withdrawn", "stayed": "Stayed" } ``` The disposition list for HLRs and SCs through Caseflow ```javascript! export const DISPOSITION_OPTIONS = ['Granted', 'Denied', 'DTA Error', 'Dismissed', 'Withdrawn']; ``` LegacyAppeals is a bit more difficult since the data comes out of vacols but the issues last action type should have to match one of the values from this hash in the AppealsSeriesIssues model ```ruby! LAST_ACTION_TYPE_FOR_DISPOSITIONS = { allowed: [ :allowed ], denied: [ :denied ], remand: [ :remanded, :manlincon_remand ], field_grant: [ :benefits_granted_by_aoj, :advance_allowed_in_field ], withdrawn: [ :withdrawn, :motion_to_vacate_withdrawn, :withdrawn_from_remand, :recon_motion_withdrawn, :advance_withdrawn_by_appellant_rep, :advance_failure_to_respond, :remand_failure_to_respond, :ramp_opt_in ] }.freeze ``` The one exception to that is that :cavc_remand is also a possible type added by the method itself under certain conditions ```ruby! def last_action_for_issues(issues) issues.reduce(date: nil, type: nil) do |memo, issue| if issue.close_date && (memo[:date].nil? || issue.close_date > memo[:date]) type = last_action_type_from_disposition(issue.disposition) if type # Prevent draft decisions from being shared publicly unless [:allowed, :denied, :remand].include?(type) && issue.appeal.activated? memo[:date] = issue.close_date memo[:type] = type end end end last_cavc_remand = issue.cavc_decisions.select(&:remanded?).max_by(&:decision_date) if last_cavc_remand && (memo[:date].nil? || last_cavc_remand.decision_date > memo[:date]) memo[:date] = last_cavc_remand.decision_date memo[:type] = :cavc_remand end memo end end ``` ##### Possible values for HLRs and SCs should be ```ruby! ['Granted', 'Denied', 'DTA Error', 'Dismissed', 'Withdrawn'] ``` ##### Possible values for Appeals should be ```ruby! ["allowed", "remanded", "denied", "vacated", "dismissed_death", "dismissed_matter_of_law", "withdrawn", "stayed"] ``` ##### Possible values for legacy appeals should be ```ruby! [:allowed, :denied, :remanded, :manlincon_remand, :benefits_granted_by_aoj, :advance_allowed_in_field, :withdrawn, :motion_to_vacate_withdrawn, :withdrawn_from_remand, :recon_motion_withdrawn, :advance_withdrawn_by_appellant_rep, :advance_failure_to_respond, :remand_failure_to_respond, :ramp_opt_in, :cavc_remand] ``` ### `docket` #### Attribute description An attribute that contains docket information based on the type of docket, whether or not it's eligible for docket switch, and receipt date of the Appeal **Special Note: This attribute only applies to Appeal and Legacy Appeals not to HLRs or SCs** #### Possible values `object`: An object that contains docket information described above and below #### Code snippets ```ruby # For Appeals def docket_hash return unless active_status? return if location == "aoj" { type: fetch_docket_type, month: Date.parse(receipt_date.to_s).change(day: 1), switchDueDate: docket_switch_deadline, eligibleToSwitch: eligible_to_switch_dockets? } end # For legacy appeals it is built a little differently based on data retrieved from vacols def fetch_docket return unless active? && %w[original post_remand].include?(type_code) && form9_date && !aod DocketSnapshot.latest.docket_tracer_for_form9_date(form9_date) end # It should eventually end up looking like these DocketTracer hashes def to_hash { front: at_front, total: docket_count, ahead: ahead_count, ready: ahead_and_ready_count, month: month, docketMonth: latest_docket_month, eta: nil } end ``` ### `events` #### Attribute description Generated by the `AppealEvents` model, differing for each decision review. #### Possible values `array`: An array of objects generated by the AppealEvents class #### Code snippets ```ruby def events @events ||= AppealEvents.new(appeal: self).all end # HLR events for example def hlr_events [ hlr_request_event, hlr_decision_event, hlr_dta_error_event, dta_decision_event, hlr_other_close_event ].flatten.uniq.select(&:valid?) end def hlr_decision_event AppealEvent.new(type: :hlr_decision, date: appeal.decision_event_date) end # The appeal event to_hash method def to_hash { type: type, date: date.to_date } end # So the resulting value would look like for one of the hlr events { type: :hlr_decision, date: appeal.decision_event_date } ``` ### `evidence` #### Attribute description Always an empty array; a stubbed method. #### Possible values `array` always an empty array #### Code snippets ```ruby attribute :evidence do [] end ``` ### `type` #### Attribute description The type attribute only exists on Appeals and LegacyAppeals. It is the steam type of the appeal or 'Original'. The values are pulled from the `client/constants/AMA_STREAM_TYPES.json` file for appeals. It is comes from these values for LegacyAppeals ```ruby TYPE_CODES = { "Original" => "original", "Post Remand" => "post_remand", "Reconsideration" => "reconsideration", "Court Remand" => "post_cavc_remand", "Clear and Unmistakable Error" => "cue" }.freeze ``` #### Possible values **Appeals:** `string`: ["Original", "Vacate", "De Novo", "Court Remand"] **Legacy Appeals:** `string`: ["original", "post_remand", "reconsideration", "post_cavc_remand", "cue", "other"] #### Code snippets ```ruby enum stream_type: { Constants.AMA_STREAM_TYPES.original.to_sym => Constants.AMA_STREAM_TYPES.original, Constants.AMA_STREAM_TYPES.vacate.to_sym => Constants.AMA_STREAM_TYPES.vacate, Constants.AMA_STREAM_TYPES.de_novo.to_sym => Constants.AMA_STREAM_TYPES.de_novo, Constants.AMA_STREAM_TYPES.court_remand.to_sym => Constants.AMA_STREAM_TYPES.court_remand } def type stream_type&.titlecase || "Original" end { "original": "original", "vacate": "vacate", "de_novo": "de_novo", "court_remand": "court_remand" } # For legacy appeals # Codes for Appeals Status API TYPE_CODES = { "Original" => "original", "Post Remand" => "post_remand", "Reconsideration" => "reconsideration", "Court Remand" => "post_cavc_remand", "Clear and Unmistakable Error" => "cue" }.freeze def type_code TYPE_CODES[type] || "other" end ``` ## Additional Considerations This document serves as a structured reference for API field definitions and their implementations. Some fields (e.g., `events`, `alerts`, `status` details) may require further exploration depending on use cases.