> ## Documentation Index
> Fetch the complete documentation index at: https://developers.mypilotindex.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Push the pilot's flight totals

> Sends the logbook's current totals for the pilot of a grant that holds hours.write: per aircraft type, in the logbook's own categories and in minutes, and the simulator sessions per device class. It is a whole state, not a delta. MyPilotIndex applies its own aviation rules (SPIC is PIC time, PICUS has its own column, a turboshaft helicopter counts in the helicopter turbine columns and not in airplane turbine, and simulator time never counts as flight time), raises each of the pilot's hour columns to the logbook's value when it is higher and never lowers one, raises the hours in type of the type ratings he holds, and recomputes his MPI Score when anything moved. A type's category is read only when the MyPilotIndex aircraft catalog does not resolve the type, and such a type adds to no engine-type column (jet, turboprop, turbine): a helicopter counts in the helicopter columns and in no engine column; an airplane counts in the airplane total and follows multi_engine in the engine-count columns (single engine, multi engine, PIC multi engine); a glider, balloon, airship, gyroplane or powered lift adds to total time and its role columns only, never to an engine-count column. A type the catalog resolves follows the catalog, whatever its category says. The push carries at least one types row or one simulator row (a logbook of simulator sessions only sends types as an empty array). The grant keeps only aggregates of the push (the resulting columns and counts), never a type designator or a row. as_of is required and names the instant of the logbook's state; it may not be more than 5 minutes past the server's clock, and it is compared to the microsecond. Sending the same as_of and body again (a retry after a lost answer) answers 200 and applies nothing while that push is still the grant's last accepted push; after a newer push the same retry is 409 stale_totals. A retry is recognized by its as_of and by the totals it sums to, not by its bytes: two different bodies sent with one as_of that sum to the same totals are treated as the same push, and the second applies nothing. A push whose as_of is older than the grant's last accepted push is 409 stale_totals; one accepted push per grant per 15 minutes, otherwise 429 push_throttled with Retry-After. 422 hours_rule when the totals, merged with the pilot's record, would break his hours rules (for example a figure above 99,999.9 hours): nothing is applied. Every call counts toward the daily allowance. Pushes take no Idempotency-Key. A grant that is revoked, unknown or another partner grant answers 404 not_found; a grant, or a token, without hours.write is 403 scope_missing.



## OpenAPI

````yaml /api-reference/partner.yaml put /v1/partner/grants/{grant_id}/flight-totals
openapi: 3.1.0
info:
  title: MyPilotIndex Partner API
  version: 1.0.0
  description: >-
    The API of approved partner applications, such as logbooks, that act for a
    pilot who has granted them access on the MyPilotIndex consent screen. It is
    a separate document from the member API (openapi.yaml): partner routes live
    under /v1/partner/ and are never served to browsers. Authenticate with an
    access token from the identity provider's token endpoint
    (grant_type=client_credentials, scopes partner/<scope>) in the Authorization
    header. Every authenticated answer carries X-RateLimit-Limit and
    X-RateLimit-Remaining, the partner's daily allowance and what is left of it
    (a 401, and a path or method outside this document, carry neither); past the
    allowance the API answers 429 rate_limited with Retry-After. Errors use the
    shared envelope below. The API is additive within v1: new fields and routes
    may appear, existing ones do not change meaning.
servers:
  - url: https://{devApiHost}
    description: >-
      Sandbox on the AWS dev environment, with synthetic pilots. The host is
      issued to the partner with its sandbox credential.
    variables:
      devApiHost:
        default: dev-api.mypilotindex.com
        description: The dev API host given to the partner with the sandbox credential.
security: []
tags:
  - name: Grants
    description: >-
      A pilot connects on MyPilotIndex: the partner sends his browser to the
      consent page
      (https://mypilotindex.com/connect/{slug}?client_id&redirect_uri&scope&state&code_challenge&code_challenge_method=S256),
      the pilot signs in and allows, and the browser returns to the registered
      redirect_uri with code and state (or error=access_denied and state when he
      denies). The partner exchanges the code here for a grant and the pilot's
      pairwise id, which signs the pilot in ("Sign in with MyPilotIndex") and
      names him on every later call. A grant that is revoked, unknown or another
      partner's answers 404 on every route, identically.
  - name: Certificates
    description: >-
      Check a certificate a pilot shows you, such as his MPI Report, by its
      public id. The answer is the public verification's, and the call needs no
      grant.
  - name: Webhooks
    description: >-
      MyPilotIndex tells a partner about its grants by webhook: grant.revoked
      when a grant ends, and ping when the partner asks for a test. The webhook
      URL (https only) and its signing secret are set by MyPilotIndex when the
      partner is approved; the secret is shown once. Each event is a POST of the
      event as JSON (Content-Type application/json) signed in the Standard
      Webhooks scheme: webhook-id (the event id), webhook-timestamp and
      webhook-signature, "v1," and the base64 HMAC-SHA256 of
      "<webhook-id>.<webhook-timestamp>.<body>" under the key the secret carries
      in base64 after its whsec_ prefix, over the exact bytes received. Verify
      the signature, refuse a timestamp more than five minutes from your clock,
      dedupe on webhook-id, and answer any 2xx within 10 seconds. A 5xx, a 408,
      a 429, a redirect (never followed), a timeout or a connection failure is
      retried with growing gaps for about a day (twelve attempts), with the same
      webhook-id and a fresh timestamp and signature. A 4xx other than 408 and
      429 is taken as a refusal and not retried, except for a grant.revoked
      event with reason account_deleted: its grant leaves every list with the
      account, so it cannot be reconciled later, and it is retried within the
      same window on any answer but a 2xx, a 4xx included. A partner that missed
      other events reconciles with GET
      /v1/partner/grants?status=revoked&changed_since=.
  - name: Publishing
    description: >-
      Text a partner publishes: a post or a story as a pilot who granted
      feed.write or stories.write, and a post on the organization page the
      partner is bound to, with org.posts. Text only: no media, no YouTube link,
      no hangar, never anonymous. Every create takes a client_ref, 1 to 64 of
      letters, digits and . _ : - , unique per partner and kind: the same ref
      with the same body answers 200 with the stored id and replayed true,
      whatever the allowance says, so a retry after a lost answer is always
      safe; the same ref with another body, or on another grant, is 409
      client_ref_reused. Bodies are compared after normalizing, so surrounding
      spaces do not make two creates. Text may not carry an @handle or @all,
      because a mention notifies people: 422 mentions_not_allowed. A personal
      e-mail address in the text is 400 personal_contact; a company address is
      fine. Rolling allowances count what was published in the last 24 hours,
      removed or not, on top of the daily allowance of every call: 3 posts and 5
      stories per grant, 10 organization posts per partner; past them 429
      publication_throttled with Retry-After. A partner removes only what it
      created, and on a grant only what it created on that grant: anything else
      is the 404 of a row that does not exist, and so is a row the pilot already
      removed. A removal answers 204, and a create it removed still answers its
      id on a retry, with replayed true. A grant whose pilot may not publish
      right now, because his account is suspended or its deletion is pending,
      answers 404 on these routes as a revoked grant does, for as long as that
      lasts, though it stays active in GET /v1/partner/grants. A partner's posts
      and stories look like the pilot's own: no answer of the member API names
      the partner.
  - name: Jobs
    description: >-
      The active listings of MyPilotIndex, and the jobs of the company a partner
      is bound to. jobs.read and jobs.write are the partner's own scopes:
      MyPilotIndex allows them per partner, on the token alone, and no pilot
      sees them. A job goes live at once, without the review queue, as a
      company's job does: the partner is a company MyPilotIndex approved under a
      data agreement. Publishing needs the binding to a company, which
      MyPilotIndex sets; without one, 403 partner_not_bound. Every create takes
      a client_ref, 1 to 64 of letters, digits and . _ : - , unique per partner:
      the same ref with the same body answers 200 with the stored id and
      replayed true, whatever the allowance says, so a retry after a lost answer
      is always safe; the same ref with another body is 409 client_ref_reused.
      Bodies are compared after normalizing, so member order, surrounding spaces
      and the spelling of a number do not make two creates. A rolling allowance
      counts what was published in the last 24 hours, closed or not, on top of
      the daily allowance of every call: 50 jobs per partner; past it 429
      publication_throttled with Retry-After. A partner changes and closes only
      what it created, while the job's company is the one it is bound to:
      anything else is the 404 of a row that does not exist. Job text carries no
      mention rule and no contact guard: it does not reach the feed.
  - name: Insights
    description: >-
      The Workforce: what the pilots of MyPilotIndex look like as a market, in
      aggregates. insights.read is the partner's own scope, for the six screens;
      insights.position is a pilot's, for one pilot against his cohort. Every
      figure comes through the assembly the member screens use, so the floors
      are the server's: a cohort under the floor answers status insufficient and
      no section, a cell under its floor is suppressed, and a filter that would
      leave too few pilots is dropped, which filters.broadened says. No answer
      carries a count of pilots: a cohort is a band.
paths:
  /v1/partner/grants/{grant_id}/flight-totals:
    put:
      tags:
        - Grants
      summary: Push the pilot's flight totals
      description: >-
        Sends the logbook's current totals for the pilot of a grant that holds
        hours.write: per aircraft type, in the logbook's own categories and in
        minutes, and the simulator sessions per device class. It is a whole
        state, not a delta. MyPilotIndex applies its own aviation rules (SPIC is
        PIC time, PICUS has its own column, a turboshaft helicopter counts in
        the helicopter turbine columns and not in airplane turbine, and
        simulator time never counts as flight time), raises each of the pilot's
        hour columns to the logbook's value when it is higher and never lowers
        one, raises the hours in type of the type ratings he holds, and
        recomputes his MPI Score when anything moved. A type's category is read
        only when the MyPilotIndex aircraft catalog does not resolve the type,
        and such a type adds to no engine-type column (jet, turboprop, turbine):
        a helicopter counts in the helicopter columns and in no engine column;
        an airplane counts in the airplane total and follows multi_engine in the
        engine-count columns (single engine, multi engine, PIC multi engine); a
        glider, balloon, airship, gyroplane or powered lift adds to total time
        and its role columns only, never to an engine-count column. A type the
        catalog resolves follows the catalog, whatever its category says. The
        push carries at least one types row or one simulator row (a logbook of
        simulator sessions only sends types as an empty array). The grant keeps
        only aggregates of the push (the resulting columns and counts), never a
        type designator or a row. as_of is required and names the instant of the
        logbook's state; it may not be more than 5 minutes past the server's
        clock, and it is compared to the microsecond. Sending the same as_of and
        body again (a retry after a lost answer) answers 200 and applies nothing
        while that push is still the grant's last accepted push; after a newer
        push the same retry is 409 stale_totals. A retry is recognized by its
        as_of and by the totals it sums to, not by its bytes: two different
        bodies sent with one as_of that sum to the same totals are treated as
        the same push, and the second applies nothing. A push whose as_of is
        older than the grant's last accepted push is 409 stale_totals; one
        accepted push per grant per 15 minutes, otherwise 429 push_throttled
        with Retry-After. 422 hours_rule when the totals, merged with the
        pilot's record, would break his hours rules (for example a figure above
        99,999.9 hours): nothing is applied. Every call counts toward the daily
        allowance. Pushes take no Idempotency-Key. A grant that is revoked,
        unknown or another partner grant answers 404 not_found; a grant, or a
        token, without hours.write is 403 scope_missing.
      operationId: partners.push_flight_totals.v1
      parameters:
        - name: grant_id
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        description: >-
          Maximum 262144 bytes (256 KiB). Unknown or repeated members are
          rejected at every level.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FlightTotalsInput'
            example:
              as_of: '2026-11-04T12:00:00Z'
              logbook_authority: ANAC
              types:
                - aircraft_type: B738
                  category: airplane
                  multi_engine: true
                  minutes:
                    total: 61200
                    pic: 0
                    sic: 61200
                    picus: 0
                    spic: 0
                    dual_received: 0
                    solo: 0
                    instructor: 0
                    night: 9000
                    actual_instrument: 4200
                    simulated_instrument: 0
                    cross_country: 58000
                - aircraft_type: R44
                  category: helicopter
                  multi_engine: false
                  minutes:
                    total: 3000
                    pic: 2400
                    dual_received: 600
              simulator:
                - device: FFS
                  minutes:
                    trainee: 480
                    instructor: 0
      responses:
        '200':
          description: >-
            What the push did. A retry of the last accepted push, and a push
            that brings nothing new, answer applied 0 and score_requested false.
          headers:
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushResult'
              example:
                applied: 9
                unchanged: 3
                dropped: []
                score_requested: true
        '400':
          description: >-
            invalid_request: a malformed body (an unknown or repeated member, a
            value out of range, more than 500 types, no types row and no
            simulator row, one type or one device class twice, an as_of that is
            not RFC 3339 or is more than 5 minutes ahead).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          $ref: '#/components/responses/Error'
        '403':
          description: >-
            forbidden with reason scope_missing; the grant or the token does not
            hold hours.write.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ScopeMissingError'
        '404':
          $ref: '#/components/responses/Error'
        '405':
          $ref: '#/components/responses/Error'
        '409':
          description: >-
            conflict with reason stale_totals; as_of is older than the as_of of
            the grant's last accepted push. Nothing was applied. Send the
            logbook's current state.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StaleTotalsError'
              example:
                error:
                  code: conflict
                  message: Request conflicts with an earlier operation.
                  correlation_id: 0af7651916cd43dd8448eb211c80319c
                  reason: stale_totals
        '413':
          $ref: '#/components/responses/Error'
        '422':
          description: >-
            unprocessable with reason hours_rule; the totals, merged with the
            pilot's record, would break his hours rules. Nothing was applied.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HoursRuleError'
              example:
                error:
                  code: unprocessable
                  message: Request cannot be applied.
                  correlation_id: 0af7651916cd43dd8448eb211c80319c
                  reason: hours_rule
        '429':
          description: >-
            rate_limited. With reason push_throttled, the grant's last accepted
            push was less than 15 minutes ago and Retry-After names when the
            next push is taken; with reason rate_limited, the partner's daily
            allowance is used up and Retry-After names the next UTC midnight.
            Nothing was applied.
          headers:
            Retry-After:
              description: The HTTP date, in UTC, from which the call is taken again.
              schema:
                type: string
                example: Wed, 04 Nov 2026 12:15:00 GMT
            X-RateLimit-Limit:
              $ref: '#/components/headers/RateLimitLimit'
            X-RateLimit-Remaining:
              $ref: '#/components/headers/RateLimitRemaining'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PushRateLimitedError'
              example:
                error:
                  code: rate_limited
                  message: Too many requests.
                  correlation_id: 0af7651916cd43dd8448eb211c80319c
                  reason: push_throttled
        '500':
          $ref: '#/components/responses/Error'
        '503':
          $ref: '#/components/responses/Error'
        '504':
          $ref: '#/components/responses/Error'
      security:
        - partnerOAuth: []
components:
  schemas:
    FlightTotalsInput:
      type: object
      additionalProperties: false
      required:
        - as_of
        - types
      properties:
        as_of:
          type: string
          format: date-time
          description: >-
            The instant of the logbook's state (RFC 3339). Not more than 5
            minutes past the server's clock; compared to the microsecond.
        logbook_authority:
          type: string
          enum:
            - FAA
            - EASA
            - ANAC
            - TCCA
            - CASA
            - other
          description: >-
            Informative; the authority whose rules the logbook keeps. Not
            stored.
        types:
          type: array
          minItems: 0
          maxItems: 500
          description: >-
            One row per aircraft type the logbook knows, each type once. It may
            be empty for a logbook of simulator sessions only: a push carries at
            least one types row or one simulator row (400 invalid_request
            otherwise).
          items:
            $ref: '#/components/schemas/FlightTotalsType'
        simulator:
          type: array
          maxItems: 5
          description: >-
            Sessions in a flight simulation training device, one row per device
            class. Kept apart from flight time.
          items:
            $ref: '#/components/schemas/SimulatorSession'
    PushResult:
      type: object
      additionalProperties: false
      required:
        - applied
        - unchanged
        - dropped
        - score_requested
      properties:
        applied:
          type: integer
          minimum: 0
          description: >-
            How many of the pilot's figures the push raised: hour columns, and
            hours in type of the type ratings he holds.
        unchanged:
          type: integer
          minimum: 0
          description: >-
            How many hour columns of the push the pilot's record already held at
            or above it.
        dropped:
          type: array
          items:
            type: string
          description: >-
            The hour columns left out, sorted, because within the push a column
            was larger than its parent (for example PIC above total time); its
            children are left out with it.
        score_requested:
          type: boolean
          description: >-
            Whether the pilot's MPI Score is recomputed; true exactly when
            applied is above zero.
    Error:
      type: object
      additionalProperties: false
      required:
        - error
      properties:
        error:
          type: object
          additionalProperties: false
          required:
            - code
            - message
            - correlation_id
          properties:
            code:
              type: string
              enum:
                - invalid_request
                - request_too_large
                - unauthenticated
                - not_found
                - conflict
                - unprocessable
                - operation_disabled
                - rate_limited
                - forbidden
                - dependency_unavailable
                - internal
                - deadline_exceeded
                - method_not_allowed
            message:
              type: string
              description: Stable safe English message, never the underlying error text.
            correlation_id:
              type: string
              pattern: ^[a-f0-9]{32}$
            reason:
              type: string
              description: Optional refusal reason from the operation's closed vocabulary.
    ScopeMissingError:
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          properties:
            error:
              type: object
              properties:
                reason:
                  type: string
                  enum:
                    - scope_missing
    StaleTotalsError:
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          properties:
            error:
              type: object
              properties:
                reason:
                  type: string
                  enum:
                    - stale_totals
    HoursRuleError:
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          properties:
            error:
              type: object
              properties:
                reason:
                  type: string
                  enum:
                    - hours_rule
    PushRateLimitedError:
      allOf:
        - $ref: '#/components/schemas/Error'
        - type: object
          properties:
            error:
              type: object
              properties:
                reason:
                  type: string
                  enum:
                    - rate_limited
                    - push_throttled
    FlightTotalsType:
      type: object
      additionalProperties: false
      required:
        - aircraft_type
        - category
        - minutes
      properties:
        aircraft_type:
          type: string
          minLength: 1
          maxLength: 64
          description: >-
            The type as the logbook spells it: the ICAO designator is preferred;
            the MyPilotIndex catalog also resolves FAA designators and aliases.
            Surrounding spaces are ignored and the comparison is case blind.
        category:
          type: string
          enum:
            - airplane
            - helicopter
            - glider
            - balloon
            - airship
            - gyroplane
            - powered_lift
          description: >-
            Read only when the catalog does not resolve the type: a helicopter
            counts in the helicopter columns, an airplane in the airplane total
            and in the engine-count columns multi_engine names, and a glider,
            balloon, airship, gyroplane or powered lift in total time and its
            role columns only. An unresolved type adds to no engine-type column
            (jet, turboprop, turbine).
        multi_engine:
          type:
            - boolean
            - 'null'
          description: >-
            Whether the type is multi engine; null or absent when the logbook
            does not know. Used for the engine-count columns of a type the
            catalog resolves (other than a helicopter) and of an unresolved
            airplane; ignored for every other unresolved category.
        minutes:
          $ref: '#/components/schemas/FlightTotalsMinutes'
    SimulatorSession:
      type: object
      additionalProperties: false
      required:
        - device
        - minutes
      properties:
        device:
          type: string
          enum:
            - FFS
            - FTD
            - FNPT
            - ATD
            - other
          description: >-
            The device class. AATD, BATD, BITD and PCATD are ATD. Trainee time
            in an FFS also counts as full flight simulator time.
        minutes:
          type: object
          additionalProperties: false
          description: >-
            Minutes as a trainee and as an instructor; instructor time is
            accepted and not stored in v1.
          properties:
            trainee:
              $ref: '#/components/schemas/Minutes'
            instructor:
              $ref: '#/components/schemas/Minutes'
    FlightTotalsMinutes:
      type: object
      additionalProperties: false
      description: >-
        The career totals of one type in minutes, each an integer from 0 to
        6,000,000 (100,000 hours); an absent member is 0. spic (student pilot in
        command, solo) counts as PIC time; picus (PIC under supervision) has its
        own column and is never added to PIC.
      properties:
        total:
          $ref: '#/components/schemas/Minutes'
        pic:
          $ref: '#/components/schemas/Minutes'
        sic:
          $ref: '#/components/schemas/Minutes'
        picus:
          $ref: '#/components/schemas/Minutes'
        spic:
          $ref: '#/components/schemas/Minutes'
        dual_received:
          $ref: '#/components/schemas/Minutes'
        solo:
          $ref: '#/components/schemas/Minutes'
        instructor:
          $ref: '#/components/schemas/Minutes'
        night:
          $ref: '#/components/schemas/Minutes'
        actual_instrument:
          $ref: '#/components/schemas/Minutes'
        simulated_instrument:
          $ref: '#/components/schemas/Minutes'
        cross_country:
          $ref: '#/components/schemas/Minutes'
    Minutes:
      type: integer
      minimum: 0
      maximum: 6000000
  headers:
    RateLimitLimit:
      description: >-
        The partner's daily allowance of calls (1,000 unless agreed otherwise),
        per UTC day.
      schema:
        type: integer
    RateLimitRemaining:
      description: What is left of the allowance after this call.
      schema:
        type: integer
  responses:
    Error:
      description: The shared error envelope.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
  securitySchemes:
    partnerOAuth:
      type: oauth2
      description: >-
        OAuth 2.0 client credentials. Request a token from the token endpoint of
        the identity provider's domain (https://{cognito-domain}/oauth2/token,
        with HTTP Basic client authentication) and send it as a Bearer token.
        Production uses auth.mypilotindex.com. The sandbox uses the dev
        environment's Cognito domain, which is given to the partner with its
        sandbox credential. A token carries only the scopes the partner was
        approved for.
      flows:
        clientCredentials:
          tokenUrl: https://auth.mypilotindex.com/oauth2/token
          scopes:
            partner/profile: Read the pilot's name, photo and username.
            partner/score: Read the pilot's MPI Score and phase.
            partner/hours.read: Read the pilot's flight hours.
            partner/hours.write: Update the pilot's flight hours with the logbook's totals.
            partner/licenses: Read the pilot's licenses, ratings and type ratings.
            partner/applications: Read the pilot's applications to jobs from the partner's company.
            partner/insights.position: Read how the pilot compares with his cohort.
            partner/jobs.match: Read the active jobs that fit the pilot best.
            partner/feed.write: Publish text posts as the pilot.
            partner/stories.write: Publish text stories as the pilot.
            partner/jobs.read: >-
              Read active job listings. MyPilotIndex allows it per partner,
              never a pilot.
            partner/jobs.write: >-
              Publish jobs for the partner's company. MyPilotIndex allows it per
              partner, never a pilot.
            partner/org.posts: >-
              Post on the partner's organization page. MyPilotIndex allows it
              per partner, never a pilot.
            partner/insights.read: >-
              Read Workforce screens. MyPilotIndex allows it per partner, never
              a pilot.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.