medianfi-com Webhooks API

The Webhooks API from medianfi-com — 3 operation(s) for webhooks.

Operations 3

POST /webhooks/inbound-email Accept a signed SendGrid Inbound Parse delivery #
POST /webhooks/slack/events Accept a signed Slack Events API delivery #
POST /webhooks/github/secret-scanning Revoke service keys that GitHub secret scanning reports as leaked #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/medianfi-com:medianfi-com-webhooks-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

medianfi-com-webhooks-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: Median Webhooks API
  version: 0.1.0
  description: Production-grade Node.js + Express API starter template
  license:
    name: Proprietary
servers:
- url: /api/v1
security: []
tags:
- name: Webhooks
paths:
  /webhooks/inbound-email:
    post:
      operationId: ingestInboundEmail
      summary: Accept a signed SendGrid Inbound Parse delivery
      description: Provider-only document intake, mounted only when ONBOARDING_V3_INBOUND_EMAIL_ENABLED is true. This route has no bearer session. The provider signs the timestamp bytes followed by the exact raw multipart bytes; Median verifies that signature before parsing, resolving the single company alias recipient, scanning attachments, and writing documents. Provider retries are idempotent by immutable delivery identity.
      tags:
      - Webhooks
      security: []
      parameters:
      - name: X-Twilio-Email-Event-Webhook-Timestamp
        in: header
        required: true
        description: Ten-digit Unix timestamp included in the signed payload and checked for staleness.
        schema:
          type: string
          pattern: ^\d{10}$
      - name: X-Twilio-Email-Event-Webhook-Signature
        in: header
        required: true
        description: Base64-encoded ECDSA P-256 signature over the timestamp followed by the exact raw body.
        schema:
          type: string
          minLength: 4
          maxLength: 256
          pattern: ^[A-Za-z0-9+/]+={0,2}$
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              type: object
              additionalProperties: true
              description: Standard SendGrid Inbound Parse text fields plus dynamically named attachment file parts are accepted. `envelope` and `headers` are required, `headers` must contain exactly one Message-ID, and at least one binary file part such as `attachment1` is required. Every text field is limited to 262144 bytes; body, attachment-size, attachment-count, field, and part ceilings are configured server-side.
              required:
              - envelope
              - headers
              properties:
                envelope:
                  type: string
                  maxLength: 262144
                  description: JSON string with a non-empty `from` string and a `to` string array containing exactly one Median inbound alias.
                headers:
                  type: string
                  maxLength: 262144
                  description: Raw message headers containing exactly one Message-ID header.
                from:
                  type: string
                  maxLength: 262144
                  description: Provider-supplied display sender; routing authority comes from `envelope`.
                to:
                  type: string
                  maxLength: 262144
                subject:
                  type: string
                  maxLength: 262144
                text:
                  type: string
                  maxLength: 262144
                html:
                  type: string
                  maxLength: 262144
                attachments:
                  type: string
                  maxLength: 262144
                  description: Provider-supplied attachment count; file parts remain authoritative.
                attachment-info:
                  type: string
                  maxLength: 262144
                  description: Provider-supplied attachment metadata JSON.
                attachment1:
                  type: string
                  format: binary
                  description: Representative attachment file part; additional valid file-part names are accepted.
      responses:
        '200':
          description: Inbound attachments accepted for durable document intake
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InboundEmailIngestApiResponse'
        '400':
          $ref: '#/components/responses/InboundEmailBadRequest'
        '401':
          $ref: '#/components/responses/InboundEmailUnauthorized'
        '404':
          $ref: '#/components/responses/InboundEmailNotFound'
        '413':
          $ref: '#/components/responses/InboundEmailPayloadTooLarge'
        '415':
          $ref: '#/components/responses/InboundEmailUnsupportedMedia'
        '422':
          $ref: '#/components/responses/InboundEmailUnprocessableEntity'
        '503':
          $ref: '#/components/responses/InboundEmailServiceUnavailable'
  /webhooks/slack/events:
    post:
      operationId: ingestSlackEvent
      summary: Accept a signed Slack Events API delivery
      description: Inbound half of the bidirectional Slack thread mirror, mounted only when SLACK_SIGNING_SECRET is set. This route has no bearer session. Slack signs the exact raw request bytes, so the route parses the body as raw and verifies the HMAC before any JSON decoding. Deliveries are acked immediately and processed asynchronously, because Slack retries anything not acked within three seconds; retries are idempotent on (channel, message timestamp). Messages authored by the Median bot are ignored, which is what stops the mirror from echoing forever.
      tags:
      - Webhooks
      security: []
      parameters:
      - name: X-Slack-Request-Timestamp
        in: header
        required: true
        description: Unix timestamp included in the signature base string; rejected beyond five minutes of skew.
        schema:
          type: string
          pattern: ^\d{9,11}$
      - name: X-Slack-Signature
        in: header
        required: true
        description: Hex HMAC-SHA256 over "v0:{timestamp}:{raw body}", keyed with the app signing secret.
        schema:
          type: string
          pattern: ^v0=[a-f0-9]{64}$
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
              - type
              properties:
                type:
                  type: string
                  enum:
                  - url_verification
                  - event_callback
                  description: url_verification is the one-time Request URL handshake.
                challenge:
                  type: string
                  description: Echoed back verbatim during the url_verification handshake.
                team_id:
                  type: string
                  description: Slack workspace ID; joined with the channel to resolve the Median company.
                event:
                  type: object
                  additionalProperties: true
                  description: The Slack event. Only `message` events are acted on; app_mention would arrive as a duplicate delivery of the same message.
                  properties:
                    type:
                      type: string
                    subtype:
                      type: string
                    channel:
                      type: string
                    user:
                      type: string
                    bot_id:
                      type: string
                    text:
                      type: string
                    ts:
                      type: string
                    thread_ts:
                      type: string
                      description: Present on replies; equals the parent message ts, which is how a Slack thread maps to a Median thread.
      responses:
        '200':
          description: Delivery accepted. Returns the challenge during url_verification, otherwise an empty body. Always returned once the signature is valid, including when downstream processing fails, so Slack does not retry.
          content:
            application/json:
              schema:
                type: object
                properties:
                  challenge:
                    type: string
        '400':
          description: Body was not valid JSON
        '401':
          description: Missing, stale, or invalid Slack signature
  /webhooks/github/secret-scanning:
    post:
      operationId: receiveGithubSecretScanningAlert
      summary: Revoke service keys that GitHub secret scanning reports as leaked
      description: GitHub secret-scanning partner callback (#1147). Mounted only when GITHUB_SECRET_SCANNING_ALERTS_ENABLED=true; otherwise 404. There is no bearer session. GitHub signs the exact raw request bytes with an ECDSA P-256 key named in Github-Public-Key-Identifier; the key is fetched from https://api.github.com/meta/public_keys/secret_scanning and the signature is verified before the body is parsed. Only a token that parses as the `mdn_svc_v1_` service format (checksum included) and matches its stored hash exactly is revoked; every other token is labelled false_positive and nothing changes. Repeat deliveries are idempotent. Token values are never logged or echoed; the response carries SHA-256 hashes.
      tags:
      - Webhooks
      security: []
      parameters:
      - name: Github-Public-Key-Identifier
        in: header
        required: true
        description: Identifier of the GitHub signing key.
        schema:
          type: string
      - name: Github-Public-Key-Signature
        in: header
        required: true
        description: Base64 DER ECDSA (SHA-256) signature over the raw body.
        schema:
          type: string
      requestBody:
        required: true
        description: At most 64kb and 100 alerts.
        content:
          application/json:
            schema:
              type: array
              minItems: 1
              maxItems: 100
              items:
                type: object
                required:
                - token
                - type
                properties:
                  token:
                    type: string
                    maxLength: 512
                  type:
                    type: string
                    maxLength: 100
                  url:
                    type: string
                    maxLength: 2048
                  source:
                    type: string
                    maxLength: 100
      responses:
        '200':
          description: Token analysis, one entry per distinct token (GitHub partner response format).
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  required:
                  - token_hash
                  - token_type
                  - label
                  properties:
                    token_hash:
                      type: string
                      pattern: ^[0-9a-f]{64}$
                      description: Hex SHA-256 of the reported token.
                    token_type:
                      type: string
                    label:
                      type: string
                      enum:
                      - true_positive
                      - false_positive
        '401':
          description: Missing signature, unknown signing key, or invalid signature
        '404':
          description: The callback is not enabled
        '413':
          description: Body larger than 64kb
        '422':
          description: Body is not a valid alert array
        '503':
          description: GitHub signing keys could not be fetched and none is cached; GitHub retries
components:
  schemas:
    ApiErrorResponse:
      type: object
      required:
      - success
      - error
      properties:
        success:
          type: boolean
          enum:
          - false
        error:
          $ref: '#/components/schemas/ProblemDetail'
    ProblemDetail:
      type: object
      properties:
        type:
          type: string
        title:
          type: string
          description: 'For application errors, a stable UPPER_SNAKE machine code (for example `NOT_FOUND`, `FORECAST_NOT_FOUND`, `VALIDATION_ERROR`). Clients branch on this, never on `detail` wording. A few transport level responses use a short human title instead.

            '
        status:
          type: integer
        detail:
          type: string
          description: 'Human-readable explanation; correlate a failure with `traceId`. A NOT_FOUND-family 404 names the resource type (for example "Company not found"), not its id. Some legacy messages still include an id (tracked in #2666).

            '
        instance:
          type: string
        traceId:
          type: string
        retryAfter:
          type: integer
          minimum: 0
          description: Whole seconds until retry is allowed for a 429 response
        errors:
          type: array
          items:
            type: object
            properties:
              field:
                type: string
              message:
                type: string
        extensions:
          type: object
          additionalProperties: true
    InboundEmailIngestApiResponse:
      type: object
      additionalProperties: false
      properties:
        success:
          type: boolean
          const: true
        data:
          $ref: '#/components/schemas/InboundEmailIngestResult'
      required:
      - success
      - data
    InboundEmailIngestResult:
      type: object
      additionalProperties: false
      properties:
        accepted:
          type: boolean
          const: true
        companyId:
          type: string
          format: uuid
        created:
          type: integer
          minimum: 0
        duplicates:
          type: integer
          minimum: 0
      required:
      - accepted
      - companyId
      - created
      - duplicates
  responses:
    InboundEmailUnprocessableEntity:
      description: An attachment was rejected after malware scanning
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
    InboundEmailUnsupportedMedia:
      description: The body is not raw multipart/form-data or uses a non-identity content encoding
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
    InboundEmailServiceUnavailable:
      description: Inbound intake is temporarily at its bounded concurrency capacity
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
    InboundEmailUnauthorized:
      description: The provider signature headers are missing, invalid, or stale
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
    InboundEmailNotFound:
      description: The gated route or exact allowlisted company alias is not available
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
    InboundEmailBadRequest:
      description: The signed multipart body, envelope, message headers, recipient, or attachment metadata is invalid
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
    InboundEmailPayloadTooLarge:
      description: The raw body, a field, an attachment, or the multipart field, file, or part count exceeds its configured limit
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ApiErrorResponse'
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    serviceApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: 'Persistent credential for the configured cloud-bookkeeping service principal. The key must carry accountant:* and is accepted only on accountant operations that explicitly list this scheme. Ordinary API keys, and service keys presented to bearer-only operations, are denied.

        '
    refreshSessionCookie:
      type: apiKey
      in: cookie
      name: __Secure-median_refresh
      description: 'Exact production HttpOnly refresh-session cookie. Local non-secure development uses median_refresh.

        '
    passkeyEnrollmentGrantCookie:
      type: apiKey
      in: cookie
      name: __Secure-median_passkey_enrollment
      description: 'Exact production HttpOnly passkey-enrollment grant cookie. Local non-secure development uses median_passkey_enrollment.

        '
    webAuthnCeremonyCookie:
      type: apiKey
      in: cookie
      name: __Secure-median_webauthn
      description: 'Exact production HttpOnly WebAuthn ceremony cookie. Local non-secure development uses median_webauthn.

        '
x-median-quarantined-authorities:
  decision: ADR-0001-model-a
  runtimeStatus: unmounted
  description: 'Migrations 267-272 modeled legal entities as separate company tenants. These paths are not runtime API authority while their preserved accounting math is re-keyed to legal entities inside one company workspace.

    '
  paths:
  - /companies/{companyId}/consolidation-groups
  - /companies/{companyId}/consolidation-groups/{groupId}/versions/{groupVersionId}/report-rollups
  - /companies/{companyId}/consolidation-groups/{groupId}/versions/{groupVersionId}/report-rollups/{rollupId}
  - /companies/{companyId}/consolidation-groups/{groupId}/versions/{groupVersionId}/translation-adjustments
  - /companies/{companyId}/consolidation-groups/{groupId}/versions/{groupVersionId}/translation-adjustments/{adjustmentId}
x-median-demo-parity-customer-surface:
  runtimeStatus: mounted-lifecycle-only
  description: 'v2 customer-app capabilities available to JWT-authenticated members of any active or test company — the v2 app is the product, not a rollout cohort, so no allowlist applies to the markerless path. Production customer calls omit X-Median-App-Surface. Test-company calls may carry X-Median-App-Surface: qa to select the QA bundle; that marker branch alone still requires membership in CUSTOMER_APP_DEMO_PARITY_COMPANY_IDS. An API key, unknown marker, malformed company id, inactive company, missing gate dependency, or failed company lookup receives the standard hidden-resource 404 contract. The held surfaces riding these prefixes (AR/AP, Forecast, Insights) carry their own additional allowlist gate — see x-median-forecast-hold and x-median-ar-ap-hold.

    '
  headerContract: '#/components/parameters/demoParityCustomerAppSurface'
  routePrefixes:
  - /companies/{companyId}/ar
  - /companies/{companyId}/ap
  - /companies/{companyId}/customers
  - /companies/{companyId}/vendors
  - /companies/{companyId}/contracts
  - /companies/{companyId}/receivables
  - /companies/{companyId}/entities
  - /companies/{companyId}/currencies
  - /companies/{companyId}/reporting-scopes
  - /companies/{companyId}/reporting-scopes/{reportingScopeId}/versions/{versionId}/rollups
  - /companies/{companyId}/reporting-scopes/{reportingScopeId}/versions/{versionId}/translation-adjustments
  - /companies/{companyId}/presentation-report-snapshots
  - /companies/{companyId}/reporting-trial-balances
  - /companies/{companyId}/reporting-scopes/{reportingScopeId}/versions/{versionId}/trial-balances
  - /companies/{companyId}/reporting-scopes/{reportingScopeId}/versions/{versionId}/intercompany
  - /companies/{companyId}/weekly-email
  - /companies/{companyId}/forecast
  - /companies/{companyId}/books
  - /companies/{companyId}/sub-ledgers
x-median-forecast-hold:
  runtimeStatus: mounted-only-with-server-gate
  description: 'Forecast is held. Beyond the lifecycle-only customer surface above, every operation under the prefix below also requires the company to appear in the held-surface allowlist CUSTOMER_APP_DEMO_PARITY_COMPANY_IDS — the one operator-set list that opens AR/AP, Forecast and Insights (the legacy CUSTOMER_APP_FORECAST_COMPANY_IDS variable is no longer consulted). It is empty by default, so the v2 rollout alone never exposes Forecast. A company off the list receives the same hidden-resource 404 contract as one for which the surface does not exist. The auth-context featureFlags key `forecast` mirrors this verdict for the customer app shell.

    '
  featureFlag: forecast
  routePrefixes:
  - /companies/{companyId}/forecast
x-median-ar-ap-hold:
  runtimeStatus: mounted-only-with-server-gate
  description: 'Receivables/Payables are held behind TWO required authorities. Beyond the lifecycle-only customer surface above, every operation under the prefixes below requires the company to appear in the held-surface allowlist CUSTOMER_APP_DEMO_PARITY_COMPANY_IDS (requireDemoParityAllowlistedCompany) AND to satisfy the company_subledger_flags conversion chain (requireArApItemsEnabled — the same authority the public /pay page, aging and the accountant portal consult); failing either answers the opaque 404. The auth-context featureFlags key `arApItems` mirrors exactly that conjunction, resolved server-side, so the customer shell hides the Receivables/Payables nav and pages instead of offering menu entries these routes 404. Default is FALSE for every company while the list is empty or the conversion has not run.

    '
  featureFlag: arApItems
  routePrefixes:
  - /companies/{companyId}/ar
  - /companies/{companyId}/ap
  - /companies/{companyId}/customers
  - /companies/{companyId}/vendors
  - /companies/{companyId}/contracts
  - /companies/{companyId}/receivables
x-median-demo-parity-qa-only:
  runtimeStatus: mounted-only-with-server-gate
  description: 'Strict test-company-only routes. Every interactive call requires X-Median-App-Surface: qa in addition to JWT authentication, the server allowlist, and persisted client_status=test. A copied header, API key, active company, missing gate dependency, or failed company lookup receives the standard hidden-resource 404 contract.

    '
  headerContract: '#/components/parameters/qaAppSurface'
  routePrefixes:
  - /companies/{companyId}/gl-accounts/{id}/monetary-classification
  - /companies/{companyId}/fx-account-roles
  - /companies/{companyId}/gl-periods/{periodId}/fx-revaluation
  - /accountant/companies/{customerId}/gl-accounts/{id}/monetary-classification
  - /accountant/companies/{customerId}/fx-account-roles
  - /accountant/companies/{customerId}/gl-periods/{periodId}/fx-revaluation
  - /accountant/companies/{customerId}/ar/{id}
  - /accountant/companies/{customerId}/ap/{id}
x-median-onboarding-v3-surface:
  runtimeStatus: promoted
  promotionFlag: ONBOARDING_V3_QA_ONLY=false
  description: 'Promoted. The deployment this contract describes runs ONBOARDING_V3_QA_ONLY=false, so none of the onboarding-v3 auth, pricing, or setup paths below requires X-Median-App-Surface: qa. An unmarked request is served and reaches ordinary authentication, so an unauthenticated call now answers 401 rather than the hidden-resource 404. Mounting is still conditional: with ONBOARDING_V3_ENABLED false every path below returns the hidden-resource 404 before body parsing or authentication. The header stays defined and is still honoured, because a deployment left at ONBOARDING_V3_QA_ONLY=true keeps requiring it on every path below and 404s unmarked traffic; setting that flag back to true is the rollback. On either setting the header is a pre-company bundle marker, never tenant authorization.

    '
  headerContract: '#/components/parameters/onboardingV3QaSurface'
  routePaths:
  - /auth/otp/start
  - /auth/otp/verify
  - /auth/passkeys/register/options
  - /auth/passkeys/register/verify
  - /auth/passkeys/assert/options
  - /auth/passkeys/assert/verify
  - /onboarding/v3/quote
  - /onboarding/v3/offer/resolve
  - /onboarding/v3/flow
  - /onboarding/v3/setup-intent
  - /onboarding/v3/subscribe
  - /onboarding/v3/test-account-activation
  - /onboarding/v3/checkout-session
  - /onboarding/v3/checkout-session/complete
  - /onboarding/v3/setup-state
  - /onboarding/v3/delegate-invites
  - /onboarding/v3/delegate-invites/{inviteId}
  - /onboarding/v3/delegate/resolve
  - /onboarding/v3/delegate
  - /onboarding/v3/task-context
  - /onboarding/v3/delegate/task-context
  - /onboarding/v3/banks/plaid/link-token
  - /onboarding/v3/banks/plaid/exchange
  - /onboarding/v3/banks/commit
  - /onboarding/v3/documents/google-workspace/auth-url
  - /onboarding/v3/documents/google-workspace/callback
  - /onboarding/v3/books/quickbooks/auth-url
  - /onboarding/v3/books/quickbooks/callback
  - /onboarding/v3/books/xero/auth-url
  - /onboarding/v3/books/xero/callback
  - /onboarding/v3/welcome-call
  - /onboarding/v3/welcome-call/embed
  - /onboarding/v3/books-progress
  - /onboarding/v3/books/open
  - /companies/{companyId}/onboarding/v3/questions
  - /companies/{companyId}/onboarding/v3/questions/{questionId}/answer
  - /companies/{companyId}/onboarding/v3/review-items