openapi: 3.1.0
info:
  title: GrantCue API
  version: 1.0.0
  summary: Search the GrantCue grant catalog and work with your organization's pipeline.
  description: |
    The GrantCue API gives an organization programmatic access to the public grant
    catalog and to its own pipeline of saved grants.

    **Authentication.** Create an API key under Settings > Integrations > API keys
    (organization admins, Team plan and higher). Send it in the `Authorization`
    header as `Bearer <key>`. A key sent in a URL is refused with `key_in_query`.
    Keys are shown once, stored only as a hash, and can be revoked at any time.

    **Scopes.** `catalog:read`, `pipeline:read`, `pipeline:write`. A key can never
    do more than its scopes allow, and never more than the organization member who
    created it: it stops working when that member leaves the organization or the
    organization leaves a plan that includes the API.

    **Rate limits.** Per key: 120 reads and 30 writes per minute. Over the limit,
    the API answers 429 with a `Retry-After` header.

    **Visibility.** The catalog endpoints return exactly the grants the public
    GrantCue site shows, with a fixed set of public fields.
  license:
    name: Proprietary
    identifier: LicenseRef-GrantCue-Terms
servers:
  - url: https://www.grantcue.com/api/v1
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Catalog
    description: The public grant catalog.
  - name: Pipeline
    description: The key's organization's saved grants.
paths:
  /catalog:
    get:
      tags: [Catalog]
      operationId: searchCatalog
      summary: Search the grant catalog
      description: Same filters as Discover. Requires the `catalog:read` scope.
      security:
        - bearerAuth: [catalog:read]
      parameters:
        - name: q
          in: query
          description: Keywords, an opportunity number, or a quoted phrase. At most 200 characters.
          schema: { type: string, maxLength: 200 }
        - name: agency
          in: query
          description: Funding agency or organization (partial match).
          schema: { type: string, maxLength: 200 }
        - name: funding_category
          in: query
          schema: { type: string, maxLength: 100 }
        - name: status
          in: query
          description: One or more of posted, forecasted, closed, archived, separated by commas. Defaults to posted and forecasted.
          schema: { type: string, examples: ["posted,forecasted"] }
        - name: eligibility
          in: query
          description: Grants.gov applicant type code.
          schema: { type: string, maxLength: 10 }
        - name: min_funding
          in: query
          schema: { type: number, minimum: 0 }
        - name: max_funding
          in: query
          schema: { type: number, minimum: 0 }
        - name: aln
          in: query
          description: Assistance Listing Number.
          schema: { type: string, maxLength: 20 }
        - name: due_in_days
          in: query
          description: Only grants closing within this many days.
          schema: { type: integer, minimum: 1, maximum: 365 }
        - name: source
          in: query
          description: Source keys, separated by commas.
          schema: { type: string }
        - name: location
          in: query
          description: A location page slug.
          schema: { type: string, pattern: "^[a-z0-9-]{1,80}$" }
        - name: include_expired
          in: query
          schema: { type: boolean, default: false }
        - name: sort
          in: query
          schema: { type: string, enum: [relevance, due_soon, newest] }
        - name: page_size
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, maximum: 100000, default: 0 }
      responses:
        "200":
          description: A page of grants.
          headers:
            X-RateLimit-Limit: { $ref: "#/components/headers/RateLimitLimit" }
            X-RateLimit-Remaining: { $ref: "#/components/headers/RateLimitRemaining" }
            X-RateLimit-Reset: { $ref: "#/components/headers/RateLimitReset" }
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/Grant" }
                  meta: { $ref: "#/components/schemas/PageMeta" }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /catalog/{id}:
    get:
      tags: [Catalog]
      operationId: getGrant
      summary: Get one grant
      description: Requires the `catalog:read` scope. A grant the public site does not show answers 404.
      security:
        - bearerAuth: [catalog:read]
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: string, format: uuid }
      responses:
        "200":
          description: The grant.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: { $ref: "#/components/schemas/Grant" }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
  /pipeline:
    get:
      tags: [Pipeline]
      operationId: listPipeline
      summary: List the organization's pipeline
      description: Requires the `pipeline:read` scope. Notes, assignees and descriptions are not included.
      security:
        - bearerAuth: [pipeline:read]
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [researching, go-no-go, drafting, submitted, awarded, not-funded, closed-out, rejected, withdrawn, archived]
        - name: page_size
          in: query
          schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
        - name: offset
          in: query
          schema: { type: integer, minimum: 0, maximum: 100000, default: 0 }
      responses:
        "200":
          description: A page of saved grants.
          content:
            application/json:
              schema:
                type: object
                required: [data, meta]
                properties:
                  data:
                    type: array
                    items: { $ref: "#/components/schemas/PipelineGrant" }
                  meta: { $ref: "#/components/schemas/PageMeta" }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
    post:
      tags: [Pipeline]
      operationId: addToPipeline
      summary: Add a catalog grant to the pipeline
      description: |
        Requires the `pipeline:write` scope. The grant enters the first stage
        ("researching"), saved as the member who created the key. Adding a grant
        that is already in the pipeline is a no-op and answers 200. The plan's
        limit on active opportunities applies. Nothing can be edited or deleted
        through the API.
      security:
        - bearerAuth: [pipeline:write]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: false
              required: [grant_id]
              properties:
                grant_id:
                  type: string
                  format: uuid
                  description: The `id` of a catalog grant.
                priority:
                  type: string
                  enum: [low, medium, high, urgent]
      responses:
        "200":
          description: The grant was already in the pipeline.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PipelineWriteResult" }
        "201":
          description: The grant was added.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PipelineWriteResult" }
        "400": { $ref: "#/components/responses/Error" }
        "401": { $ref: "#/components/responses/Error" }
        "402": { $ref: "#/components/responses/Error" }
        "403": { $ref: "#/components/responses/Error" }
        "404": { $ref: "#/components/responses/Error" }
        "409": { $ref: "#/components/responses/Error" }
        "429": { $ref: "#/components/responses/RateLimited" }
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: gck_ followed by 43 URL-safe characters
      description: An organization API key. Send it only in the Authorization header.
  headers:
    RateLimitLimit:
      description: Requests allowed in the current window.
      schema: { type: integer }
    RateLimitRemaining:
      description: Requests left in the current window.
      schema: { type: integer }
    RateLimitReset:
      description: When the window resets, in Unix seconds.
      schema: { type: integer }
  responses:
    Error:
      description: The request failed. `error.code` is stable and machine-readable.
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
    RateLimited:
      description: Too many requests for this key. Wait for `Retry-After` seconds.
      headers:
        Retry-After:
          description: Seconds to wait.
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: "#/components/schemas/Error" }
  schemas:
    Error:
      type: object
      required: [error, request_id]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              examples: [invalid_api_key, key_in_query, insufficient_scope, plan_required, key_owner_removed, invalid_parameter, invalid_body, not_found, plan_limit_reached, rate_limited, internal_error]
            message: { type: string }
        request_id:
          type: string
          description: Quote this when contacting support.
    PageMeta:
      type: object
      required: [total, page_size, offset, has_more]
      properties:
        total: { type: integer }
        page_size: { type: integer }
        offset: { type: integer }
        has_more: { type: boolean }
    Grant:
      type: object
      required: [id, source, title, status]
      properties:
        id: { type: string, format: uuid }
        source: { type: string, description: Source key. }
        source_name: { type: string }
        external_id: { type: string }
        opportunity_number: { type: [string, "null"] }
        title: { type: string }
        description: { type: [string, "null"] }
        agency: { type: [string, "null"] }
        status: { type: string, examples: [posted] }
        funding_category: { type: [string, "null"] }
        estimated_funding: { type: [number, "null"] }
        award_floor: { type: [number, "null"] }
        award_ceiling: { type: [number, "null"] }
        expected_awards: { type: [integer, "null"] }
        cost_sharing_required: { type: [boolean, "null"] }
        eligibility_applicants:
          type: [array, "null"]
          items: { type: string }
        aln_codes:
          type: [array, "null"]
          items: { type: string }
        cfda_numbers:
          type: [array, "null"]
          items: { type: string }
        posted_date: { type: [string, "null"], format: date-time }
        open_date: { type: [string, "null"], format: date-time }
        close_date: { type: [string, "null"], format: date-time }
        source_url: { type: [string, "null"], format: uri }
        application_url: { type: [string, "null"], format: uri }
        last_updated_at: { type: [string, "null"], format: date-time }
    PipelineGrant:
      type: object
      properties:
        id: { type: string, format: uuid }
        catalog_grant_id: { type: [string, "null"], format: uuid }
        title: { type: string }
        agency: { type: [string, "null"] }
        status:
          type: string
          enum: [researching, go-no-go, drafting, submitted, awarded, not-funded, closed-out, rejected, withdrawn, archived]
        priority: { type: [string, "null"], enum: [low, medium, high, urgent, null] }
        open_date: { type: [string, "null"], format: date-time }
        close_date: { type: [string, "null"], format: date-time }
        loi_deadline: { type: [string, "null"], format: date-time }
        internal_deadline: { type: [string, "null"], format: date-time }
        saved_at: { type: string, format: date-time }
    PipelineWriteResult:
      type: object
      required: [data, meta]
      properties:
        data:
          oneOf:
            - { $ref: "#/components/schemas/PipelineGrant" }
            - { type: "null" }
        meta:
          type: object
          required: [created]
          properties:
            created: { type: boolean }
