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: 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