# Hodoflow developer documentation > Choose the right API surface, complete a safe first request, and move from outcome-focused guides into exact machine contracts. Reference documentation for the Hodoflow HTTP API. Append `.md` to any page URL for its markdown source. ## Get started - [Build with Hodoflow](https://developer.hodoflow.com/index.md): Choose the right API surface, complete a safe first request, and move from outcome-focused guides into exact machine contracts. - [Make your first API request](https://developer.hodoflow.com/get-started/first-api-request.md): Create read-only service-account access, exchange it for a short-lived token, and verify a bounded workflow-list response. - [Choose an API surface](https://developer.hodoflow.com/get-started/choose-an-api.md): Match configuration, warehouse reads, and inbound event delivery to the correct Hodoflow API surface and prerequisites. - [Base URLs, workspaces, and versions](https://developer.hodoflow.com/get-started/base-urls-workspaces-versions.md): Build canonical Hodoflow API URLs and understand how service-account workspace context and surface-specific versions are selected. ## Authentication - [Authentication](https://developer.hodoflow.com/authentication.md): Create workspace-scoped service accounts, exchange client credentials, and operate short-lived bearer tokens with least privilege. - [Exchange service-account credentials](https://developer.hodoflow.com/authentication/token-exchange.md): Call the client-credentials token endpoint safely, interpret its response, and cache short-lived bearer tokens without a refresh token. - [Scopes and run-as permissions](https://developer.hodoflow.com/authentication/scopes-and-permissions.md): Apply Hodoflow's two-layer authorization model and choose the least-privilege scope for each API surface and operation. - [Rotate and revoke credentials](https://developer.hodoflow.com/authentication/rotation-and-revocation.md): Roll service-account secrets without downtime, promote the new pair, and revoke access with predictable runtime effect. ## Guides - [List and inspect workflows](https://developer.hodoflow.com/guides/list-workflows.md): Retrieve a bounded workflow collection, select one resource, and inspect its exact JSON:API attributes and response headers. - [Follow a workflow run](https://developer.hodoflow.com/guides/follow-workflow-run.md): Find a recent workflow run, retrieve its current state, and inspect safe per-step outcomes without exposing payloads. - [Perform an idempotent write](https://developer.hodoflow.com/guides/idempotent-write.md): Create one test dashboard through JSON:API, replay the identical request safely, and recognize a conflicting key reuse. ## API reference - [API reference](https://developer.hodoflow.com/reference): Browse the generated JSON:API operation reference. ## Platform behavior - [JSON:API documents and media types](https://developer.hodoflow.com/platform/json-api.md): Read and write Hodoflow's path-versioned JSON:API documents with the correct envelopes, headers, resource identities, and relationships. - [Pagination, filtering, sorting, and sparse fields](https://developer.hodoflow.com/platform/pagination-filtering-sorting.md): Bound JSON:API collections explicitly and combine operation-declared filters, sort fields, includes, and sparse fieldsets safely. - [Errors and request IDs](https://developer.hodoflow.com/platform/errors-request-ids.md): Distinguish JSON:API, GraphQL, token, and webhook failures; use stable codes, correlation IDs, and retryability without leaking credentials. - [Idempotency and retries](https://developer.hodoflow.com/platform/idempotency-retries.md): Retry Hodoflow operations safely by separating JSON:API response replay, webhook delivery deduplication, read retries, and permanent failures. - [Rate limits](https://developer.hodoflow.com/platform/rate-limits.md): Interpret configurable Hodoflow request windows, Retry-After responses, and the separate budgets for APIs and webhook ingestion. - [Versioning and changelog](https://developer.hodoflow.com/platform/versioning-changelog.md): Understand Hodoflow's surface-specific compatibility policy, response version headers, deprecation signals, and release-matched artifacts. ## GraphQL - [GraphQL warehouse API](https://developer.hodoflow.com/graphql.md): Use the read-only factQuery operation to retrieve latest or historical warehouse records for an existing data model. - [Run your first warehouse query](https://developer.hodoflow.com/graphql/first-query.md): Obtain a real data-model slug, send a bounded factQuery with variables, and verify empty or populated GraphQL results. - [factQuery arguments and results](https://developer.hodoflow.com/graphql/fact-query.md): Understand latest versus timeline semantics and every public factQuery argument, PageOfFact field, and Fact field. - [GraphQL filters, pagination, and limits](https://developer.hodoflow.com/graphql/filters-pagination-limits.md): Encode factQuery filters safely, page with explicit limits and offsets, and stay within time-window, complexity, depth, and token guards. ## Webhook ingestion - [Webhook ingestion](https://developer.hodoflow.com/webhook-ingestion.md): Send authenticated inbound deliveries to saved, active webhook workflows and follow the runs they create. - [Send your first webhook delivery](https://developer.hodoflow.com/webhook-ingestion/first-delivery.md): Configure an active webhook workflow, send one idempotent JSON delivery, and verify the accepted run through JSON:API. - [Webhook authentication and signatures](https://developer.hodoflow.com/webhook-ingestion/authentication-signatures.md): Combine the trigger URL, selected service-account bearer token, and optional API-key or raw-body HMAC verification safely. - [Webhook deduplication, retries, and troubleshooting](https://developer.hodoflow.com/webhook-ingestion/retries-troubleshooting.md): Choose stable delivery IDs, interpret accepted duplicates, and retry only transient webhook-ingestion failures. ## Tools and downloads - [Tools and downloads](https://developer.hodoflow.com/tools.md): Download the public OpenAPI and GraphQL contracts, browse the raw API viewer, or consume the Markdown and LLM indexes. - [OpenAPI contract](https://developer.hodoflow.com/tools/openapi.md): Download and use Hodoflow's release-matched HTTP contract for JSON:API, token exchange, and webhook ingestion. - [GraphQL SDL contract](https://developer.hodoflow.com/tools/graphql-sdl.md): Download Hodoflow's versioned GraphQL schema and use it for validation and type generation with the runtime rules kept in view. ## Specifications - [OpenAPI document](https://developer.hodoflow.com/openapi.json): Machine-readable description of every exposed JSON API path. - [GraphQL schema](https://developer.hodoflow.com/schema.graphql): SDL for the GraphQL transport. - [Product documentation](https://hodoflow.com/help): Help Center guides for the product these APIs drive. ## Optional - [Full documentation](https://developer.hodoflow.com/llms-full.txt): Every page on this site inlined as one markdown document. --- # Build with Hodoflow > Choose the right API surface, complete a safe first request, and move from outcome-focused guides into exact machine contracts. Hodoflow exposes three developer surfaces for three different jobs. Start with the outcome you need, not with a transport name. Every authenticated surface uses a workspace-scoped service account and short-lived bearer token, but their prerequisites, versions, limits, and response shapes remain distinct. ## Start with a verified success New to the API? [Make your first API request](/get-started/first-api-request). That guide creates least-privilege `api_read` access, exchanges credentials, and lists at most ten workflows. It succeeds even when the workspace has no data: `200 OK` with `data: []` is the expected empty result. The guide shows every request, representative response, checkpoint, recovery step, and cleanup action. You need owner or admin access for service-account setup, an active run-as member, Bash, cURL, and `jq`. ## Choose an outcome ### Manage configuration or observe work Use **JSON:API** under `/api/json/v1` to inspect or manage workflows, workflow runs, data models, dashboards, integrations, users, organizations, memberships, and service-account metadata. - Start: [List and inspect workflows](/guides/list-workflows) - Observe: [Follow a workflow run](/guides/follow-workflow-run) - Write safely: [Perform an idempotent write](/guides/idempotent-write) - Reference: [Browse JSON:API resources](/reference/json-api/workflows) ### Query warehouse records Use **GraphQL** at `POST /api/graphql` for the read-only `factQuery` operation. You need an existing data model slug and warehouse records, so this is not the empty-workspace quickstart. - Start: [Run your first warehouse query](/graphql/first-query) - Learn: [Filters, pagination, and limits](/graphql/filters-pagination-limits) - Reference: [Inspect factQuery](/reference/graphql/fact-query) ### Send an event into a workflow Use **webhook ingestion** at `POST /api/v1/webhooks/:token`. First create, save, and activate a webhook workflow, then authenticate with the exact service account selected on its trigger and the `webhooks` scope. - Start: [Send your first delivery](/webhook-ingestion/first-delivery) - Secure: [Sender authentication and signatures](/webhook-ingestion/authentication-signatures) - Reference: [Inspect the delivery endpoint](/reference/webhooks/ingest) Read [Choose an API surface](/get-started/choose-an-api) for a detailed decision table and [Base URLs, workspaces, and versions](/get-started/base-urls-workspaces-versions) before deploying an integration. ## Authentication in one minute An owner or admin creates a service account in the intended workspace, chooses an active run-as member, and grants only the scopes the integration needs. The client ID and secret are exchanged at `POST /api/v1/oauth/token` for a bearer token. The default lifetime is 900 seconds, but clients must trust the returned `expires_in` value. No refresh token is issued. On every API request Hodoflow reloads and validates the service account, its scopes, workspace, and run-as membership. Deactivation, revocation, membership changes, and scope removal therefore take effect without waiting for an already minted token to expire. Continue with [Authentication](/authentication). ## Contracts and tools The [curated API reference](/reference) is generated from the same enriched contract as the machine artifacts. It shows exact operation scopes, parameters, objects, examples, success semantics, stable errors, and retry behavior. Its request builder changes copyable text only; it never sends a request or stores a credential. - [OpenAPI JSON](/openapi.json) — JSON:API plus reviewed token and webhook operations - [GraphQL SDL](/schema.graphql) — the release-matched GraphQL schema - [Raw Swagger viewer](/tools/swagger) — a secondary, non-executing OpenAPI viewer - [Markdown and LLM index](/llms.txt) — every guide and machine surface - Append `.md` to any guide URL for its canonical Markdown representation Version and compatibility policy lives in [Versioning and changelog](/platform/versioning-changelog). For product setup, use the [Help Center](/help); for a traceable API failure, retain `x-request-id` and follow [Errors and request IDs](/platform/errors-request-ids). --- # Make your first API request > Create read-only service-account access, exchange it for a short-lived token, and verify a bounded workflow-list response. Authenticate and list the workflows visible to a workspace-scoped service account. The request is read-only and explicitly limited to ten results, and an empty list is a successful first result. ## What you will do 1. Select the workspace the integration should use. 2. Create a service account with only the `api_read` scope and an active run-as member. 3. Collect its client credentials without putting the secret in shell history. 4. Exchange those credentials at `POST /api/v1/oauth/token` and inspect the complete successful token response. 5. Call `GET /api/json/v1/workflows?page[limit]=10` and verify the body and response headers. This path starts with JSON:API because it works in an empty workspace. GraphQL warehouse queries require an existing data model and records; webhook ingestion requires an active webhook workflow and its selected service account. ## Prerequisites - **Application access:** the base URL of your Hodoflow deployment. The examples use `https://hodoflow.com`; self-hosted installations substitute their own application host. - **Setup role:** an organization owner or admin can create the service account. - **Run-as access:** at least one active organization member that should own the API request's permissions. - **Workspace:** know which workspace the integration should see. Service accounts are created in the workspace currently selected in the application. - **Command line:** Bash, cURL, and `jq`. Confirm them with `curl --version`, `jq --version`, and `bash --version`. Use a private, disposable terminal session. Do not put a client secret or bearer token in a URL, command-line argument, source file, `.env` file, shell history, browser storage, screenshot, or support message. Use your normal secret manager for a real integration. ## Request ### 1. Create read-only access in the intended workspace In the application, switch to the workspace the integration should use. Then go to **Settings → Service Accounts**, choose **New Service Account**, and set: - **Name:** a specific integration name, such as `Reporting sync`; - **Scopes:** select **API read** only; and - **Run as:** select the intended active organization member. Choose **Create Service Account**. You should land on **Client Credentials** and see a client ID beginning with `cid_` and a client secret beginning with `csec_`. The workspace is not sent in an API request: it comes from the service account you just created. The run-as member still bounds everything the account can see. For the product-side setup and permission model, see [Service accounts](/help/credentials-connections/service-accounts). If no active member appears, activate or invite the intended member before continuing. ### 2. Set variables without recording the secret in history Set the application URL, then read the credentials interactively. `read -s` keeps the secret off the screen and out of the command text stored by Bash. ```bash export HODOFLOW_BASE_URL="https://hodoflow.com" read -r -p "Client ID: " CLIENT_ID read -r -s -p "Client secret: " CLIENT_SECRET printf '\n' export CLIENT_ID CLIENT_SECRET ``` Paste only when each prompt is waiting. Do not replace the `read` commands with literal credential assignments. Your current shell and its child processes can use these values, so do this on a trusted machine and clean them up at the end. You should see the client ID as you type it and no characters while entering the secret. ### 3. Exchange the credentials and inspect the response Build the JSON from environment variables and stream it to cURL. The secret is in the request body, never the URL or a process argument. ```bash TOKEN_RESPONSE="$( jq -n '{ grant_type: "client_credentials", client_id: env.CLIENT_ID, client_secret: env.CLIENT_SECRET }' | curl --silent --show-error \ --request POST \ --header "Content-Type: application/json" \ --data-binary @- \ "$HODOFLOW_BASE_URL/api/v1/oauth/token" )" printf '%s\n' "$TOKEN_RESPONSE" | jq . ``` You should see all three success fields before extracting the token: ```json { "access_token": "", "token_type": "bearer", "expires_in": 900 } ``` `expires_in` is seconds and defaults to 900, but deployments can configure it. There is no refresh token. A production client should cache the bearer token for slightly less than the returned lifetime, then exchange again. Extract the two values only after verifying that response: ```bash TOKEN="$(printf '%s\n' "$TOKEN_RESPONSE" | jq -er '.access_token')" EXPIRES_IN="$(printf '%s\n' "$TOKEN_RESPONSE" | jq -er '.expires_in')" printf 'Token received; expires in %s seconds.\n' "$EXPIRES_IN" ``` The command prints only the lifetime, not the bearer token. ### 4. List at most ten workflows Use `-i` to include the response headers and `-g` to keep cURL from treating the square brackets in `page[limit]` as a URL glob. The bearer header is streamed to cURL through standard input so it is not placed in the command's arguments. ```bash printf 'header = "Authorization: Bearer %s"\n' "$TOKEN" | curl --silent --show-error --include --globoff --config - \ --header "Accept: application/vnd.api+json" \ "$HODOFLOW_BASE_URL/api/json/v1/workflows?page[limit]=10" ``` You should see `HTTP/… 200`, `x-api-version: v1`, a non-empty `x-request-id`, and a JSON:API document with a `data` array. Keep the request ID when troubleshooting; it is the correlation ID support can trace. ## Expected response A new or empty workspace returns `200 OK`. The exact request URL is retained in the `self` link; the request ID is different for every request. ```http HTTP/2 200 content-type: application/vnd.api+json; charset=utf-8 x-api-version: v1 x-request-id: ``` ```json { "data": [], "jsonapi": { "version": "1.0" }, "links": { "first": "https://hodoflow.com/api/json/v1/workflows?page[limit]=10", "next": null, "prev": null, "self": "https://hodoflow.com/api/json/v1/workflows?page[limit]=10" }, "meta": { "page": {} } } ``` `data: []` means authentication, workspace selection, scope enforcement, and serialization all succeeded; it does not mean the request failed. If workflows already exist, `data` contains up to ten workflow resource objects instead. ## Troubleshooting ### Token exchange returns 400 The only accepted grant is `client_credentials`. Confirm the request body has that exact `grant_type`. This response is not retryable until the request is corrected. ### Token exchange or API request returns 401 For the exchange, re-copy the client ID and secret and confirm the service account is active. For the API call, the token may have expired: exchange again and retry the read once. Do not log either credential while diagnosing it. ### The API returns 403 The service account is authenticated but lacks `api_read`. An owner or admin must edit the account and grant **API read**. Retrying the same token cannot add a scope. ### The API returns 404 Check the exact path: `/api/json/v1/workflows`. A resource-specific `404` can also mean the run-as member is not allowed to see that object; Hodoflow does not reveal whether a filtered object exists. Do not retry unchanged. ### The API returns 429 Read the `Retry-After` response header, wait that many seconds, and retry the read. Keep the `x-request-id` from the failed response. Do not use a fixed rapid retry loop. ### The response is HTML or has no version header Confirm `HODOFLOW_BASE_URL` is the application host, not the developer documentation host, and that the path includes `/api/json/v1`. Before leaving the terminal, remove every sensitive variable: ```bash unset CLIENT_ID CLIENT_SECRET TOKEN TOKEN_RESPONSE EXPIRES_IN ``` ## Next steps - [Inspect workflows and follow a run](/platform/json-api) with the same `api_read` access. - Add `api_write` only when you are ready to implement an idempotent JSON:API write; keep read-only and write integrations separate where practical. - [Run a bounded GraphQL warehouse query](/graphql) after you have a data model and records. `factQuery` also requires `api_read`. - [Configure webhook ingestion](/webhook-ingestion) after creating and activating a webhook workflow; its trigger requires the exact selected service account with the `webhooks` scope. --- # Choose an API surface > Match configuration, warehouse reads, and inbound event delivery to the correct Hodoflow API surface and prerequisites. Hodoflow has related API surfaces, not one interchangeable API. Choose by the job your integration performs. Authentication is shared; versioning, media types, pagination, errors, and prerequisites are not. ## Decision table | Goal | Surface | Mount | Prerequisite | Scope | |---|---|---|---|---| | List or manage workflows, dashboards, data models, integrations, accounts, and runs | JSON:API | `/api/json/v1` | Service account in the target workspace | `api_read` or `api_write` by method | | Read latest or historical warehouse records | GraphQL | `/api/graphql` | Existing data model slug and warehouse records | `api_read` for `factQuery` | | Send an inbound delivery into an active workflow | Webhook ingestion | `/api/v1/webhooks/:token` | Saved, active webhook workflow and selected service account | `webhooks` | | Obtain a bearer token | Token exchange | `/api/v1/oauth/token` | Active service-account client credentials | No bearer scope; the resulting token carries configured scopes | If you are still unsure, start with JSON:API. A bounded workflow list is read-only and succeeds in an empty workspace, making it the deterministic way to verify the host, credential, workspace, scope, and response headers. ## JSON:API for configuration and observability JSON:API exposes reviewed resource operations under a path-versioned `v1` mount. Collection requests can filter, sort, select sparse fields, include relationships, and paginate where the individual operation declares support. Always send `Accept: application/vnd.api+json`; writes also send that media type as `Content-Type` and should carry an `Idempotency-Key`. Do not query warehouse fact rows through JSON:API. Use GraphQL for that logical table view. Begin with [List and inspect workflows](/guides/list-workflows) or browse the [JSON:API reference](/reference/json-api/workflows). ## GraphQL for warehouse reads GraphQL exposes `health` and one authenticated data operation, `factQuery`. There are no public mutations or subscription fields. `factQuery` resolves a data model by slug, returns one latest row per entity or a bounded timeline, and uses offset pagination. Its default limit is 50, maximum is 250, and complexity usually imposes a smaller useful page size. A new workspace cannot complete this path until it has a data model and facts. Follow [Run your first warehouse query](/graphql/first-query), which shows how to obtain the slug through JSON:API before querying. ## Webhook ingestion for inbound events Webhook ingestion is not an outbound event-subscription product. A sender posts JSON to the opaque trigger path shown by an active webhook workflow. The request must use the exact service account selected on that trigger, with the `webhooks` scope. A stable delivery key prevents duplicate workflow runs during the seven-day retention window. The `202` response means the delivery was accepted and a run was enqueued; it does not mean the workflow completed. Continue by retrieving the returned run through JSON:API or opening Monitor. Start with [Send your first delivery](/webhook-ingestion/first-delivery). ## Do not blend the contracts - JSON:API's `v1` path and `x-api-version: v1` do not select a GraphQL schema version. - GraphQL field errors use a GraphQL `errors` array; JSON:API plug and resource errors use an `errors` document. - JSON:API idempotency replay applies to unsafe JSON:API requests, not to read-only `factQuery`. - Webhook delivery deduplication is a separate seven-day contract. - Only GraphQL uses query complexity, nesting-depth, and token-count limits. Next, configure [Base URLs, workspaces, and versions](/get-started/base-urls-workspaces-versions) or complete the [first API request](/get-started/first-api-request). --- # Base URLs, workspaces, and versions > Build canonical Hodoflow API URLs and understand how service-account workspace context and surface-specific versions are selected. Every request goes to the **application host**, not the developer-documentation host. For Hodoflow Cloud the examples use `https://hodoflow.com`; a self-hosted deployment substitutes its configured application origin. ```bash export HODOFLOW_BASE_URL="https://hodoflow.com" ``` Keep the origin free of a trailing slash, then append one complete mount and path. The public OpenAPI document advertises the application origin while each operation already contains its full `/api/...` path. ## Canonical mounts | Surface | Canonical request | |---|---| | Token exchange | `POST $HODOFLOW_BASE_URL/api/v1/oauth/token` | | JSON:API | `$HODOFLOW_BASE_URL/api/json/v1/...` | | GraphQL | `POST $HODOFLOW_BASE_URL/api/graphql` | | Webhook ingestion | `POST $HODOFLOW_BASE_URL/api/v1/webhooks/$TRIGGER_TOKEN` | Legacy token and webhook paths containing an organization domain remain temporarily available for compatibility but are deprecated. New clients must use the canonical apex forms above. ## Workspace context comes from the service account A service account is created while one workspace is selected. That stored workspace, together with its organization and active run-as member, determines what API requests can see. Do not send a tenant, organization, or workspace ID as an authority override. A similarly named resource in another workspace is not visible through this credential. Hodoflow validates the signed credential identity, then reloads the service account, workspace, scopes, and run-as membership for each request. Claims help locate the tenant record; the mutable database state remains authoritative. Use separate service accounts when an integration needs separate workspace, scope, ownership, or revocation boundaries. Never reuse one credential merely to avoid another token exchange. ## Versions are surface-specific | Surface | Version identity | How it changes | |---|---|---| | JSON:API | `v1` in the path and `x-api-version` response header | Breaking changes require a new mount such as `/api/json/v2` | | GraphQL | Date version in `x-api-version` and the SDL header | Additive evolution in place; removals follow GraphQL deprecation | | Token exchange | `/api/v1` route family | OAuth response shape is versioned with the route | | Webhook ingestion | `/api/v1` route family | Delivery response and authentication contract are versioned with the route | Do not send a JSON:API version header to change GraphQL behavior. Do not infer a GraphQL schema date from `v1`. Record the response's `x-api-version` and `x-request-id` with integration diagnostics. ## Discovery URLs Use release-matched public artifacts rather than scraping HTML: - [OpenAPI JSON](/openapi.json) for HTTP operations and schemas; - [GraphQL SDL](/schema.graphql) for GraphQL names and types; - [LLM index](/llms.txt) and [full Markdown corpus](/llms-full.txt) for prose; - `.md` on any guide URL for that page's canonical Markdown. Continue with [Authentication](/authentication) or read the full [Versioning and changelog policy](/platform/versioning-changelog). --- # Authentication > Create workspace-scoped service accounts, exchange client credentials, and operate short-lived bearer tokens with least privilege. Authenticated Hodoflow APIs use an OAuth2-style service-account `client_credentials` exchange. There is no user-password API login, no long-lived bearer API key, and no refresh token. An owner or admin creates the service account in the intended workspace, selects an active **run-as member**, and grants the smallest applicable scopes. The client ID and secret are then exchanged for a short-lived bearer token at `POST /api/v1/oauth/token`. For the complete reproducible path, begin with [Make your first API request](/get-started/first-api-request). ## Lifecycle at a glance 1. Select the target workspace in the Hodoflow application. 2. Create a specifically named service account. 3. Choose an active run-as member whose access should bound the integration. 4. Grant `api_read`, `api_write`, or `webhooks` only as required. 5. Capture the client secret once into your normal secret manager. 6. Exchange credentials and cache the token for slightly less than `expires_in`. 7. Send `Authorization: Bearer $TOKEN` on protected requests. 8. Rotate with an overlap window, then promote the staged pair. 9. Revoke or deactivate immediately when access is no longer needed. ## Token exchange The canonical endpoint accepts JSON credentials or HTTP Basic authentication and requires `grant_type: client_credentials`. A successful response contains: ```json { "access_token": "", "token_type": "bearer", "expires_in": 900 } ``` The lifetime is configurable; `900` seconds is the default. Trust the returned value instead of hard-coding it. Cache one token per service account, refresh slightly early, and coordinate concurrent refreshes so a fleet does not stampede the exchange endpoint. See [Exchange credentials](/authentication/token-exchange). ## Runtime authorization The bearer token identifies a service account, but it does not freeze mutable authorization until expiration. On every request Hodoflow reloads and validates: - the active service account and its current scopes; - the stored organization and workspace; - the selected run-as membership and its active state; and - the requested object's own authorization policies. This makes deactivation, revocation, scope removal, workspace changes, and membership changes effective for already minted tokens. A `404` can mean an object is not visible to the run-as member; the API does not reveal filtered object existence. ## Least-privilege scopes | Scope | Purpose | |---|---| | `api_read` | Safe JSON:API methods and GraphQL `factQuery` | | `api_write` | Unsafe JSON:API create/update operations | | `webhooks` | Webhook ingestion for triggers selecting this exact service account | Scopes do not replace the run-as member's authorization. The service account must pass both checks. Read [Scopes and run-as permissions](/authentication/scopes-and-permissions) before granting write or webhook access. ## Secret handling Never put a client secret or bearer token in a URL, source file, committed `.env`, shell history, browser storage, analytics event, screenshot, generated share link, or support message. Use environment variables only as a handoff from your normal process secret store. Redact credentials and request bodies before logging; keep the non-secret `x-request-id` for correlation. For safe rollover and emergency shutdown, continue with [Rotate and revoke credentials](/authentication/rotation-and-revocation). --- # Exchange service-account credentials > Call the client-credentials token endpoint safely, interpret its response, and cache short-lived bearer tokens without a refresh token. Exchange an active service account's client ID and secret at `POST /api/v1/oauth/token`. The canonical endpoint does not include an organization domain: Hodoflow resolves the owning organization from the server-generated client ID and fails closed if that identity is ambiguous. ## JSON request Build the request body from secret-manager-provided environment variables and stream it to cURL. Do not paste a literal secret into the command or URL. ```bash jq -n '{ grant_type: "client_credentials", client_id: env.CLIENT_ID, client_secret: env.CLIENT_SECRET }' | curl --silent --show-error \ --request POST \ --header "Content-Type: application/json" \ --data-binary @- \ "$HODOFLOW_BASE_URL/api/v1/oauth/token" ``` HTTP Basic authentication is also supported for standards-compatible clients. The JSON form above makes the complete contract visible and is used by the first-request guide. ## Success response ```json { "access_token": "", "token_type": "bearer", "expires_in": 900 } ``` `expires_in` is measured in seconds. The default is 900, but deployments can configure it. There is no refresh token. A client should: 1. retain the bearer token in process memory; 2. calculate an early refresh time from the returned lifetime; 3. reuse the token until that time instead of exchanging per API call; 4. permit one concurrent worker to exchange while peers await its result; and 5. discard the token on process shutdown. The exchange response and token must be redacted from logs. The token is a bearer credential: possession is sufficient until runtime revalidation rejects the underlying account. ## Error contract | Status | Body | Meaning | Recovery | |---|---|---|---| | `400` | `{"error":"unsupported_grant_type"}` | Missing or non-`client_credentials` grant | Correct the request; do not retry unchanged | | `401` | `{"error":"invalid_client"}` | Missing, invalid, inactive, revoked, or non-promoted credentials | Re-read the configured pair and account state; do not log them | | `429` | API rate-limit error with `Retry-After` | Exchange rate window exceeded | Wait the stated number of seconds and coordinate refreshes | The endpoint intentionally does not disclose whether the client ID, secret, or service account was the invalid part. ## Use the token Send the token only in an HTTPS authorization header: ```http Authorization: Bearer ``` Never send it in a query string, cookie, webhook URL, browser storage, or page source. If an API request returns `401`, exchange once with the current client credentials. Repeated `401` responses require inspecting account activation, rotation state, and run-as membership rather than an unbounded retry loop. See the exact [token endpoint reference](/reference/authentication/token-exchange), then learn [rotation and revocation](/authentication/rotation-and-revocation). --- # Scopes and run-as permissions > Apply Hodoflow's two-layer authorization model and choose the least-privilege scope for each API surface and operation. Every protected request must pass two independent authorization layers: 1. the service account has the exact API **scope** required by the operation; 2. its active **run-as member** is authorized for the organization, workspace, and object. A broad run-as role cannot compensate for a missing API scope, and a broad scope cannot bypass the run-as member's product authorization. ## Scope matrix | Scope | Applies to | Examples | |---|---|---| | `api_read` | Safe JSON:API methods and GraphQL `factQuery` | List workflows, retrieve runs, query facts | | `api_write` | Unsafe JSON:API operations | Create a dashboard, update a data model, activate a workflow | | `webhooks` | Inbound webhook delivery | `POST /api/v1/webhooks/:token` for the trigger selecting this account | The public `health` GraphQL field requires no credential. No public GraphQL mutations or subscriptions are exposed. Each endpoint page in the [API reference](/reference) displays the exact required scope next to its method and path. ## Run-as behavior Choose a real, active organization member whose access represents the integration's intended owner. Requests execute inside the service account's stored workspace. Object policies can further restrict a read or write, such as self-only profile access or owner-only organization changes. If that membership becomes inactive, the account's requests stop succeeding even when its bearer token has not expired. Changing a member's role can also change visible objects and allowed writes immediately. Use a dedicated integration member where operational ownership needs a stable, auditable boundary. Do not select the most privileged person merely because it avoids permission design. ## How failures appear - `401 unauthorized` means authentication failed: the token is absent, invalid, expired, or the underlying account is no longer valid. - `403 forbidden` or `insufficient_scope` means the identity is known but the required scope or policy is not satisfied. - `404 not_found` can protect object existence when the run-as member cannot see a resource. Do not treat it as proof that the object does not exist elsewhere. Keep the `x-request-id` from any failed response. Never include the bearer token when sharing diagnostics. ## Least-privilege patterns - Separate reporting (`api_read`) from automation that writes (`api_write`). - Grant `webhooks` only to the account selected by the webhook trigger. - Use separate service accounts for separate workspaces and vendors. - Remove a scope before deleting an integration so runtime revalidation cuts access immediately. - Review reference pages before adding `api_write`; not every internal action is publicly exposed. Continue with [Perform an idempotent write](/guides/idempotent-write) or [Sender authentication and signatures](/webhook-ingestion/authentication-signatures). --- # Rotate and revoke credentials > Roll service-account secrets without downtime, promote the new pair, and revoke access with predictable runtime effect. Rotate client credentials on a schedule and after any suspected exposure. A staged credential pair can authenticate alongside the current pair so consumers can move without downtime; promotion ends the overlap. ## Planned rotation 1. In **Settings → Service Accounts**, open the intended account and start a rotation. 2. Capture the new client ID and secret once into the integration's secret manager. Do not remove the active pair yet. 3. Deploy the new pair to one canary client. 4. Exchange it at `/api/v1/oauth/token` and make a bounded `api_read` request. 5. Roll the new pair to every client and verify old-client traffic is gone. 6. Promote the staged credentials in Hodoflow. 7. Remove the old pair from every secret store and deployment manifest. During the overlap, both pairs identify the same service account and therefore share its workspace, run-as member, scopes, rate budget, and revocation state. Do not use staging as a way to create a second authorization boundary. ## Token behavior during rotation Already minted tokens are not an escape hatch. Hodoflow reloads the current service account and run-as state on each request. Promotion invalidates use of the old client pair for future exchanges; deactivation or revocation makes existing tokens fail runtime validation. Clients should handle a single `401` by discarding the cached token and exchanging with the currently deployed pair. If exchange also returns `invalid_client`, stop retrying and alert: the configuration is stale, the account is inactive, or the rollout was incomplete. ## Emergency revocation When a secret or token may be exposed: 1. deactivate or revoke the service account immediately; 2. remove the credential from the external system and secret manager; 3. retain non-secret request IDs and audit timestamps for investigation; 4. create or rotate to a new account only after confirming the required workspace, run-as member, and least-privilege scopes; and 5. test with the bounded first request before re-enabling automation. Do not wait for the default 900-second token lifetime. Runtime account revalidation is designed to make revocation effective sooner. ## Membership and scope changes Removing `api_write` or `webhooks`, deactivating the run-as membership, or changing object permissions also affects already minted tokens. Prefer the narrowest change that addresses the incident, but remember an integration may cache a token: verify the protected API call, not only the token exchange. For response handling and correlation, see [Errors and request IDs](/platform/errors-request-ids). For a reproducible post-rotation canary, repeat [Make your first API request](/get-started/first-api-request). --- # List and inspect workflows > Retrieve a bounded workflow collection, select one resource, and inspect its exact JSON:API attributes and response headers. List a bounded set of workflows, then retrieve one resource by ID. This is a read-only way to practice collection controls and JSON:API resource identity. An empty collection is valid; the detail step is conditional on a workflow already existing. ## What you will do 1. Request at most ten workflows with an explicit sort and sparse fieldset. 2. Verify `200`, `x-api-version`, `x-request-id`, and the `data` array. 3. If a workflow exists, extract its ID without logging the bearer token. 4. Retrieve that workflow at its canonical item path. ## Prerequisites - Complete [Make your first API request](/get-started/first-api-request). - Keep `HODOFLOW_BASE_URL` and `TOKEN` in a trusted process environment. - Use a service account with `api_read` and an active run-as member in the workspace you intend to inspect. - Install cURL and `jq`. The workflow list reveals only objects visible in the service account's stored workspace and allowed to its run-as member. ## Request Request a small page. `--globoff` prevents cURL from interpreting bracketed query keys: ```bash WORKFLOWS_RESPONSE="$( curl --silent --show-error --globoff \ --dump-header /dev/stderr \ --header "Accept: application/vnd.api+json" \ --header "Authorization: Bearer $TOKEN" \ "$HODOFLOW_BASE_URL/api/json/v1/workflows?page[limit]=10&sort=name&fields[workflow]=name,is_active,version_number" )" printf '%s\n' "$WORKFLOWS_RESPONSE" | jq . ``` You should see a top-level `data` array containing zero to ten resources. Each resource still has `type` and `id`; the sparse fieldset restricts attributes to the requested public fields. If a workflow exists, extract the first ID and retrieve it: ```bash WORKFLOW_ID="$(printf '%s\n' "$WORKFLOWS_RESPONSE" | jq -er '.data[0].id')" curl --silent --show-error \ --header "Accept: application/vnd.api+json" \ --header "Authorization: Bearer $TOKEN" \ "$HODOFLOW_BASE_URL/api/json/v1/workflows/$WORKFLOW_ID" | jq . ``` Skip the detail request when `.data` is empty. Create a workflow through the application if the next exercise requires one; do not invent an ID. ## Expected response The collection returns `200 OK`, `x-api-version: v1`, and a non-empty `x-request-id`. A populated response resembles: ```json { "data": [ { "type": "workflow", "id": "00000000-0000-4000-8000-000000000001", "attributes": { "name": "Order intake", "is_active": true, "version_number": 3 } } ], "jsonapi": { "version": "1.0" }, "links": { "next": null, "prev": null, "self": "https://hodoflow.com/api/json/v1/workflows?page[limit]=10" } } ``` The detail response has one `data` object with the same `type` and `id`. Its attributes may be broader because the second request did not send a sparse fieldset. ## Troubleshooting - `401`: discard the cached token, exchange once, and retry once. - `403`: grant `api_read` or correct the run-as membership; retries cannot add access. - `404` on the detail path: re-read the ID in the same workspace. It may be absent or hidden by object authorization. - `400`/`422`: compare filter, sort, and field names with the [workflow reference](/reference/json-api/workflows). - `429`: wait for `Retry-After`, then repeat the read. - Empty `data`: authentication succeeded; create product state only if your next task needs it. Unset `WORKFLOW_ID` and `WORKFLOWS_RESPONSE` when finished if local policy treats resource data as sensitive. ## Next steps - [Follow a workflow run](/guides/follow-workflow-run) after the workflow executes. - [Learn pagination and filtering](/platform/pagination-filtering-sorting). - Open the [Workflows API reference](/reference/json-api/workflows) for every operation and field. --- # Follow a workflow run > Find a recent workflow run, retrieve its current state, and inspect safe per-step outcomes without exposing payloads. Observe a workflow run through JSON:API. The API exposes state, timing, and safe error metadata; it does not serialize step payloads or internal execution data. ## What you will do 1. List at most ten recent visible runs. 2. Choose one run ID. 3. Retrieve the run until it reaches a terminal or waiting state. 4. Inspect its per-step outcomes through the dedicated steps operation. ## Prerequisites - A saved workflow has executed at least once. Trigger it through its normal product path; this guide does not create a run. - A service account in the same workspace with `api_read`. - An active run-as member allowed to see the workflow. - `HODOFLOW_BASE_URL`, `TOKEN`, cURL, and `jq`. If you just sent a webhook delivery, use the returned `workflow_run_id` and skip the list step. ## Request List a small page of runs: ```bash RUNS_RESPONSE="$( curl --silent --show-error --globoff \ --header "Accept: application/vnd.api+json" \ --header "Authorization: Bearer $TOKEN" \ "$HODOFLOW_BASE_URL/api/json/v1/workflow_runs?page[limit]=10&sort=-inserted_at" )" printf '%s\n' "$RUNS_RESPONSE" | jq . RUN_ID="$(printf '%s\n' "$RUNS_RESPONSE" | jq -er '.data[0].id')" ``` Retrieve the chosen run: ```bash curl --silent --show-error \ --header "Accept: application/vnd.api+json" \ --header "Authorization: Bearer $TOKEN" \ "$HODOFLOW_BASE_URL/api/json/v1/workflow_runs/$RUN_ID" | jq . ``` The run-steps projection does not expose a `page` parameter, so it is the documented no-pagination exception. It returns the bounded set of steps from that one workflow run. Poll only when your integration truly needs completion. Use increasing delays, stay within the rate budget, and stop on a terminal state. Keep the `x-request-id` from any failure. Then inspect safe step outcomes: ```bash curl --silent --show-error \ --header "Accept: application/vnd.api+json" \ --header "Authorization: Bearer $TOKEN" \ "$HODOFLOW_BASE_URL/api/json/v1/workflow_runs/$RUN_ID/steps" | jq . ``` ## Expected response The run detail returns `200 OK` and one `workflow_run` resource. Exact public attributes are listed in the [Runs reference](/reference/json-api/runs-monitoring): ```json { "data": { "type": "workflow_run", "id": "00000000-0000-4000-8000-000000000001", "attributes": { "state": "completed", "started_at": "2026-07-31T18:30:00Z", "completed_at": "2026-07-31T18:30:02Z", "error_message": null } } } ``` The steps operation returns an array of projected step outcomes with identifier, status, timing, and safe error information. It intentionally omits step input and output payloads. ## Troubleshooting - Empty run list: the workspace has no visible runs. Execute a workflow or use Monitor to confirm the intended workspace. - `401`: exchange once and retry once. - `403`: correct `api_read` or run-as access. - `404`: the run may belong to another workspace or be hidden from the run-as member; do not scan IDs. - `429`: stop polling for `Retry-After` seconds and increase the interval. - A waiting or suspended state is not automatically a failure; inspect its public reason and the workflow's product configuration. Unset `RUNS_RESPONSE` and `RUN_ID` when finished. ## Next steps - [Send a webhook delivery](/webhook-ingestion/first-delivery) and follow its returned run ID. - [Read errors and request IDs](/platform/errors-request-ids). - Browse [Runs and monitoring operations](/reference/json-api/runs-monitoring). --- # Perform an idempotent write > Create one test dashboard through JSON:API, replay the identical request safely, and recognize a conflicting key reuse. Create one harmless dashboard configuration, replay the identical request, and prove that the second attempt does not create a duplicate. Use a disposable non-production workspace and remove the test dashboard through the application when finished. ## What you will do 1. Prepare one JSON:API dashboard document. 2. Send it with a stable `Idempotency-Key`. 3. Repeat the exact method, path, body bytes, and key. 4. Verify `idempotency-replayed: true`. 5. See why reusing that key for different input returns `409`. ## Prerequisites - A service account in the test workspace with `api_write`. - An active run-as member authorized to create dashboards. - `HODOFLOW_BASE_URL`, `TOKEN`, cURL, and `jq`. - A plan to delete the test dashboard in the Hodoflow application afterward. Do not run this tutorial against production configuration. Read [Scopes and run-as permissions](/authentication/scopes-and-permissions) before granting write access. ## Request Create the request body as a file so every retry uses identical bytes: ```bash REQUEST_FILE="$(mktemp)" IDEMPOTENCY_KEY="docs-dashboard-$(date -u +%Y%m%d)-001" jq -n '{ data: { type: "dashboard", attributes: { name: "API idempotency verification", description: "Disposable dashboard created by the developer guide" } } }' > "$REQUEST_FILE" ``` Send the first request and keep its headers visible: ```bash curl --silent --show-error --include \ --request POST \ --header "Accept: application/vnd.api+json" \ --header "Content-Type: application/vnd.api+json" \ --header "Authorization: Bearer $TOKEN" \ --header "Idempotency-Key: $IDEMPOTENCY_KEY" \ --data-binary "@$REQUEST_FILE" \ "$HODOFLOW_BASE_URL/api/json/v1/dashboards" ``` Repeat that exact command without changing the file or key. Then, only as a conflict exercise, change the dashboard name while keeping the old key and send again. The changed request must not execute. ## Expected response The first request returns `201` and one dashboard resource: ```json { "data": { "type": "dashboard", "id": "00000000-0000-4000-8000-000000000001", "attributes": { "name": "API idempotency verification", "description": "Disposable dashboard created by the developer guide" } } } ``` The identical replay returns the stored status and body with: ```http idempotency-replayed: true ``` The altered body with the same key returns `409` and the stable `idempotency_key_reused` code. It does not create or update another dashboard. Keys are retained for 24 hours by default, though deployments can configure the window. ## Troubleshooting - `401`: exchange once, then retry with the same key and body. - `403`: the account lacks `api_write`, the run-as member cannot create a dashboard, or a plan limit applies. Correct access; do not loop. - `409 idempotency_key_in_progress`: wait briefly and repeat the identical request. - `409 idempotency_key_reused`: reconcile the caller bug; do not hide it behind a new key. - `422`: compare the body with the [create dashboard reference](/reference/json-api/dashboards/createDashboard). - `429` or `5xx`: wait according to headers/backoff and retry with the same key. Remove the temporary file and sensitive variables, then delete the test dashboard through the application: ```bash rm "$REQUEST_FILE" unset REQUEST_FILE IDEMPOTENCY_KEY ``` ## Next steps - Apply the [idempotency and retry matrix](/platform/idempotency-retries) in your client. - Browse exact [dashboard operations and fields](/reference/json-api/dashboards). - Separate write credentials from read-only reporting credentials. --- # JSON:API documents and media types > Read and write Hodoflow's path-versioned JSON:API documents with the correct envelopes, headers, resource identities, and relationships. The JSON:API transport at `/api/json/v1` covers configuration and observability. It follows JSON:API 1.0 document conventions and exposes only reviewed resource actions and public fields. Warehouse fact rows belong to [GraphQL](/graphql), and inbound deliveries belong to [webhook ingestion](/webhook-ingestion). ## Request headers Every JSON:API request sends: ```http Accept: application/vnd.api+json Authorization: Bearer ``` Requests with a body also send: ```http Content-Type: application/vnd.api+json ``` Safe methods require `api_read`; unsafe create and update operations require `api_write`. The [API reference](/reference) displays the exact scope for each operation. Unsafe requests should also carry a unique `Idempotency-Key` so a retry cannot repeat a completed effect. ## Resource documents A single-resource response has one `data` object: ```json { "data": { "type": "workflow", "id": "00000000-0000-4000-8000-000000000001", "attributes": { "name": "Order intake", "is_active": true } }, "jsonapi": { "version": "1.0" } } ``` Treat the pair of `type` and `id` as the resource identity. Attributes are resource-specific and may be nullable. Only fields shown in the reference are public; internal secrets, hashes, raw payloads, share tokens, and unreviewed actions remain unavailable. A collection response has a `data` array. An empty array is a successful result, not an error: ```json { "data": [], "jsonapi": { "version": "1.0" }, "links": { "self": "https://hodoflow.com/api/json/v1/workflows?page[limit]=10", "next": null, "prev": null } } ``` Always pass an explicit limit when an operation accepts pagination. Pagination support and defaults are operation-specific; Hodoflow does not promise one global default for every generated collection. ## Write documents Write the resource type and supported attributes inside `data`: ```json { "data": { "type": "dashboard", "attributes": { "name": "Fulfillment health", "description": "Operational delivery metrics" } } } ``` Do not copy fields from a read response into a write blindly. IDs, derived values, organization/workspace context, timestamps, and read-only attributes may be server-controlled. Inspect the operation's request-body fields in the reference. ## Relationships, includes, and sparse fields Where an operation declares support: - `include=relationship.path` asks the response to sideload related resources; - `fields[type]=field_a,field_b` limits attributes for one resource type; and - relationship linkage appears under a resource object's `relationships` map. Request only what the client consumes. An empty allowed-include pattern means that operation exposes no relationship include, even if the internal resource has relationships. ## Response headers Successful and failed JSON:API responses expose: - `x-api-version: v1` — the path-versioned JSON:API contract; - `x-request-id` — the correlation ID to retain for logs and support; - `Retry-After` on rate limits and some transient failures; and - `idempotency-replayed: true` when a completed unsafe response is replayed. Continue with [Pagination, filtering, and sorting](/platform/pagination-filtering-sorting), [Errors and request IDs](/platform/errors-request-ids), and [Idempotency and retries](/platform/idempotency-retries). --- # Pagination, filtering, sorting, and sparse fields > Bound JSON:API collections explicitly and combine operation-declared filters, sort fields, includes, and sparse fieldsets safely. Collection controls are declared per JSON:API operation. Do not assume every collection has the same default, maximum, sortable fields, filter object, or relationship includes. The [API reference](/reference) is authoritative for the individual path. ## Bound every collection Use deep-object page parameters and disable cURL's bracket globbing: ```bash curl --silent --show-error --globoff \ --header "Accept: application/vnd.api+json" \ --header "Authorization: Bearer $TOKEN" \ "$HODOFLOW_BASE_URL/api/json/v1/workflows?page[limit]=10&page[offset]=0" ``` `page[limit]` is the maximum number of resources requested; `page[offset]` skips matching resources. Some operations also expose keyset values such as `page[after]` and `page[before]`. Follow returned `links.next` and `links.prev` instead of constructing cursors yourself. An operation without a `page` parameter does not accept pagination. In particular, never infer that omission means a universal default page size. ## Filters Generated filters use a deep object. The available names and operators derive from reviewed public attributes. A simple equality filter looks like: ```text filter[is_active]=true ``` Nested operators, when declared, remain under the field name. Encode values as query data; do not interpolate untrusted text into a URL by hand. A field that is readable is not automatically filterable. For user-provided filter values, use your HTTP library's query encoder. With raw cURL, use `--get --data-urlencode` so reserved characters are encoded. ## Sorting The `sort` parameter is a comma-separated list of allowed fields. Prefix a field with `-` for descending order: ```text sort=-inserted_at,name ``` Only fields matched by the operation's OpenAPI pattern are supported. Use a stable tie-breaker where the contract exposes one; otherwise follow the pagination links and do not assume ordering across concurrent writes. ## Sparse fieldsets Request only needed attributes with a fieldset keyed by JSON:API type: ```text fields[workflow]=name,is_active,version_number ``` The server still returns `type` and `id`. If included resources use another type, provide a separate fieldset for that type. Sparse fields reduce response size but do not grant access to hidden or non-public attributes. ## Includes When the operation declares relationship paths, `include` sideloads them into the top-level `included` array: ```text include=steps,triggers ``` An include unsupported by that operation is an invalid request. Avoid broad include trees in polling loops; retrieve only the relationships required for the current decision. ## Edge cases and recovery - Invalid page values, filter names, or sort patterns are client errors; correct them instead of retrying unchanged. - An empty page can be valid, especially after an offset reaches the end. - A missing `next` link means paging is complete. - Keep `x-request-id` when a generated validation error is unclear. - A `429` pauses the entire credential's request stream for `Retry-After`, not only the current page. Apply these controls in [List and inspect workflows](/guides/list-workflows). --- # Errors and request IDs > Distinguish JSON:API, GraphQL, token, and webhook failures; use stable codes, correlation IDs, and retryability without leaking credentials. Error shape depends on where a request failed. Keep the HTTP status, `x-request-id`, stable error code, and surface version together. Never attach a bearer token, client secret, trigger URL, or sensitive request body to a ticket. ## JSON:API and API plug errors Authentication, scope, rate-limit, idempotency, and JSON:API resource errors use an `errors` document. Plug-layer errors include a correlation ID in `meta`: ```json { "errors": [ { "code": "forbidden", "status": "403", "detail": "insufficient scope", "meta": { "correlation_id": "req_example" } } ] } ``` The response header's `x-request-id` is the primary value to retain. Resource validation can include source pointers or field metadata; unexpected internal errors are intentionally generic. ## GraphQL errors A parsed GraphQL request can return HTTP `200` with both `data` and an `errors` array. Validation, an unknown model slug, a bad filter, query complexity, and depth limits are field/document errors rather than JSON:API errors: ```json { "errors": [ { "message": "Operation is too complex: complexity is 300 and maximum is 200" } ] } ``` Transport failures before GraphQL execution, such as an invalid token or rate limit, use the API error envelope and their non-200 status. Check both the HTTP status and GraphQL `errors` before using `data`. ## Token and webhook errors Token exchange deliberately uses compact OAuth-style bodies such as `{"error":"invalid_client"}`. Webhook ingestion uses endpoint bodies such as `{"error":"Unauthorized"}` or `{"error":"Processing temporarily unavailable"}`. Use the endpoint reference for the exact status and retry guidance; do not parse human capitalization as a stable code. ## Stable codes ### unauthorized HTTP `401`. The bearer token is missing, invalid, expired, revoked, or no longer maps to a valid active account. Exchange once, retry once, then alert. ### missing_credentials HTTP `401` on webhook ingestion. The service-account bearer token is missing, invalid, or expired. Exchange once and retry once; repeated failure is a credential problem rather than a transient delivery failure. ### forbidden HTTP `403`. Authentication succeeded but a scope, run-as policy, or object permission failed. Correct authorization; do not retry unchanged. ### insufficient_scope HTTP `403` on webhook ingestion. The selected service account lacks `webhooks`. Grant the exact scope or select the correct account. ### invalid_client HTTP `401` from token exchange. Client credentials are absent, invalid, inactive, revoked, or in the wrong rotation state. The response does not reveal which component failed. ### unsupported_grant_type HTTP `400`. Send exactly `grant_type: client_credentials`. ### not_found HTTP `404`. The route/resource is absent or not visible to the run-as member. Do not use it to infer cross-workspace existence. ### invalid_request HTTP `422`. The JSON:API document failed stable resource validation. Correct the body before retrying. ### idempotency_key_reused HTTP `409`. The same key identified a different method/path/body fingerprint. Reconcile the client operation; do not hide the conflict by generating a new key. ### idempotency_key_in_progress HTTP `409`. Another request with the same key is still executing. Wait and retry the identical request with the same key. ### rate_limited HTTP `429`. Pause the affected credential or trigger budget for the number of seconds in `Retry-After`. ### payload_too_large HTTP `413` on webhook ingestion. Reduce the actual body below the configured limit, 1 MiB by default. Changing only `Content-Length` cannot bypass the check. ### processing_failed HTTP `422` on webhook ingestion. Stable payload or workflow validation failed. Correct the delivery before retrying. ### unsupported_trigger_authentication HTTP `422` on webhook ingestion. The trigger has a legacy Basic or Bearer connector mode that conflicts with the mandatory service-account `Authorization` header. Reconfigure the connector to API key or HMAC before retrying. ### temporarily_unavailable HTTP `503` on webhook ingestion. A transient processing dependency failed. Wait for `Retry-After`, then retry with the same delivery key. ### graphql_validation GraphQL HTTP `200` error. Correct the query, variable type, model slug, filter, or enumerated value before retrying. ### query_too_complex GraphQL HTTP `200` error. Reduce selected fields, page size, nesting depth, or document size. Current defaults are complexity 200, depth 10, and 8,000 tokens. ## Support checklist Capture the request time, method, non-secret path template, status, stable code, `x-request-id`, `x-api-version`, retry count, and deployment release. Redact query values or bodies that contain tenant data. Then follow [Idempotency and retries](/platform/idempotency-retries) before repeating work. --- # Idempotency and retries > Retry Hodoflow operations safely by separating JSON:API response replay, webhook delivery deduplication, read retries, and permanent failures. Hodoflow has two distinct duplicate-suppression contracts and ordinary retry guidance. Do not reuse one surface's behavior as a promise for another. ## JSON:API unsafe writes Send a unique `Idempotency-Key` on `POST`, `PUT`, `PATCH`, or `DELETE` requests under `/api/json`. The key is scoped by tenant and service-account principal. The request fingerprint includes method, path, and raw body. ```http Idempotency-Key: dashboard-create-20260731-001 ``` - First request: reserves the key and runs normally. - Same key and same request: replays the stored response with `idempotency-replayed: true`. - Same key and different request: returns `409 idempotency_key_reused`. - Concurrent duplicate while the first is running: returns `409 idempotency_key_in_progress`. - A `5xx` response is not retained, so a transient failure remains retryable. Completed responses are retained for 24 hours by default; deployments can configure the window. Generate keys from a stable business operation ID, not a random value on every retry. Never put a credential or tenant secret in a key. ## Webhook delivery deduplication Webhook ingestion uses a stable sender delivery identifier from `Idempotency-Key` or a supported provider header. It retains accepted delivery identity for seven days. A duplicate receives `202` with `{"status":"duplicate"}` and no new run ID. When no stable header exists, Hodoflow falls back to a time-bucketed hash of the trigger token and raw body. That protects short retries but is not a substitute for a provider delivery ID. See [Retries and troubleshooting](/webhook-ingestion/retries-troubleshooting). ## Retry matrix | Result | Retry? | Action | |---|---|---| | Network timeout before any response | Yes | Retry with the same idempotency/delivery key | | `429` | Yes | Wait the `Retry-After` duration, then retry | | `500` or `503` | Usually | Exponential backoff with jitter and a bounded attempt/time budget | | `401` | Once after exchange | Discard cached token, exchange, retry once; then alert | | `403` | No | Correct scopes or run-as permissions | | `404` | No unchanged | Correct path/ID or object access | | `409` idempotency conflict | No | Reconcile key reuse; do not generate a new key blindly | | `413` or `422` | No unchanged | Correct body size or validation failure | Reads are naturally repeatable, but still need bounded backoff. GraphQL `factQuery` is read-only and does not use JSON:API response replay. A GraphQL validation or complexity error is permanent until the document or variables change. ## Operational rules - Cap attempts and total elapsed time. - Add jitter so many workers do not retry together. - Respect the credential-wide rate budget. - Preserve method, path, body bytes, and idempotency key across retries. - Record `x-request-id`, status, attempt, and non-secret operation identity. - Never log bearer tokens, client secrets, trigger URLs, or sensitive bodies. Practice the contract in [Perform an idempotent write](/guides/idempotent-write). --- # Rate limits > Interpret configurable Hodoflow request windows, Retry-After responses, and the separate budgets for APIs and webhook ingestion. Rate limits protect API and ingestion capacity. Values below are **defaults**, not immutable product guarantees; deployments can configure them. Clients must respond to the actual `429` and `Retry-After` header. ## Default windows | Surface | Default | Key | |---|---|---| | JSON:API and authenticated GraphQL | 300 requests per 60 seconds | Service-account credential | | Public/unauthenticated API probes | 300 requests per 60 seconds | Remote IP | | Webhook ingestion | 100 deliveries per 60 seconds | Trigger token/path identity | The window is fixed. One busy endpoint can consume the same credential budget used by another endpoint, so coordinate concurrency across the integration. ## Rate-limit response JSON:API and GraphQL transport limits return a stable API error and a header: ```http HTTP/2 429 Retry-After: 17 x-request-id: ``` ```json { "errors": [ { "code": "rate_limited", "status": "429", "detail": "rate limit exceeded; retry after 17s", "meta": { "correlation_id": "" } } ] } ``` Webhook ingestion returns `429` with `Retry-After` and its simpler endpoint error body. In both cases, the header governs recovery. ## Client behavior 1. Stop dispatching new work for the affected key. 2. Wait at least the stated number of seconds. 3. Resume gradually, not with the whole backlog at once. 4. Retry unsafe work with the same idempotency or delivery key. 5. Keep a bounded attempt/time budget and add jitter. Do not guess the remaining window from local clocks. Do not retry every worker independently. A shared limiter or queue per service account produces more predictable throughput. ## Reduce request pressure - Cache bearer tokens instead of exchanging per request. - Use explicit, reasonable page sizes and follow pagination links. - Request sparse fields and only required relationships. - For GraphQL, select only consumed fields; narrower selections reduce both complexity and payload size. - Deduplicate outbound webhook retries before they reach Hodoflow. - Use exponential backoff for transient service failures as well as 429s. For general retry classification, see [Idempotency and retries](/platform/idempotency-retries). --- # Versioning and changelog > Understand Hodoflow's surface-specific compatibility policy, response version headers, deprecation signals, and release-matched artifacts. Hodoflow versions each public surface according to its transport. There is no single header that changes every API at once. ## Current public versions | Surface | Current identity | Evidence | |---|---|---| | JSON:API | `v1` | `/api/json/v1` path and `x-api-version: v1` | | GraphQL | `2026-07-05` | `x-api-version` and first line of [schema.graphql](/schema.graphql) | | Token exchange | `v1` route family | `/api/v1/oauth/token` | | Webhook ingestion | `v1` route family | `/api/v1/webhooks/:token` | The version returned by a response is the authoritative identity of that surface. Store it with `x-request-id` in diagnostics. ## JSON:API compatibility JSON:API uses major path versioning. Additive fields and operations can enter `v1`; clients must ignore attributes they do not consume. A breaking request or response change requires a new mount such as `/api/json/v2` and a migration window for `v1`. Use stable `operationId` values from [OpenAPI JSON](/openapi.json) when mapping generated clients or tests. A path can contain a version while the application origin in `servers` remains unversioned; join them exactly once. ## GraphQL compatibility GraphQL evolves additively at one `/api/graphql` endpoint. New fields are compatible for clients that select explicitly. A field or argument scheduled for removal is marked deprecated in schema introspection and the generated SDL before the deprecation window closes. The date version advances when a schema compatibility boundary changes. The schema currently exposes no mutations or subscription fields. Do not build against internal types or infer capabilities that are absent from the SDL. ## Deprecations Legacy token and webhook URLs containing an organization domain are deprecated. Use the canonical apex paths shown in [Base URLs](/get-started/base-urls-workspaces-versions). Deprecation warnings in server logs are not a client-facing migration channel; public changes must appear in release notes and the versioned artifacts. ## Release-matched changelog workflow This portal does not invent a release history separate from the product. For each deployed release: 1. compare the committed GraphQL SDL with the generated schema; 2. regenerate and contract-test OpenAPI operation IDs, scopes, examples, and errors; 3. update affected guides and reference metadata in the same change; 4. verify old HTML and Markdown aliases; and 5. publish product release notes naming additions, deprecations, and migration deadlines. Until a dedicated release-note feed is published, the current [OpenAPI](/openapi.json), [GraphQL SDL](/schema.graphql), and this page are the honest compatibility sources. Never claim a capability merely because a future route name appears in planning material. --- # GraphQL warehouse API > Use the read-only factQuery operation to retrieve latest or historical warehouse records for an existing data model. Hodoflow's GraphQL surface has one application-data job: read warehouse records through `factQuery`. It is mounted at `POST /api/graphql`, requires `api_read`, and addresses a data model by slug as a logical table. Its mapped fields are the columns and each `Fact.data` object is one row. Configuration and observability remain on [JSON:API](/platform/json-api). GraphQL exposes no mutations. The schema currently exposes no subscription fields, so there is no public subscription capability to build against. ## Before your first query Unlike the JSON:API quickstart, `factQuery` needs product state: 1. create a data model and its fields in Warehouse; 2. run a workflow that persists at least one mapped record if you want a populated result; 3. obtain the model's `slug` through JSON:API or the application; and 4. authenticate with an active service account carrying `api_read`. Follow [Run your first warehouse query](/graphql/first-query) for the exact requests and checkpoints. ## Query modes `latest` is the default and returns one current row per entity. Rows without an entity ID are excluded because no entity identity exists to collapse. `timeline` returns historical versions inside a time window. When omitted, `until` defaults to now and `since` defaults to 90 days before it. A requested span wider than 366 days is clamped to 366 days. Explicit bounds also apply in latest mode as normal filters. See [factQuery arguments and results](/graphql/fact-query) for every argument and response field. ## Pagination and cost Offset pagination defaults to 50 rows and rejects a limit above 250. Pass a limit explicitly anyway. The useful limit can be lower because complexity is based on page size and selected fields. Default configurable guards are: | Guard | Default | |---|---| | Maximum complexity | 200 | | Maximum nesting depth | 10 | | Maximum parseable tokens | 8,000 | Filters are a JSON array encoded inside a GraphQL string, at most 20 entries. Use query variables rather than hand-escaping it inline. Continue with [Filters, pagination, and limits](/graphql/filters-pagination-limits). ## Authentication and errors Send `Authorization: Bearer $TOKEN` and `Content-Type: application/json`. `factQuery` requires `api_read` and the run-as member's active workspace access. The `health` liveness field is public and is the only exception. Parsed GraphQL failures can return HTTP `200` with an `errors` array. Invalid tokens and rate limits happen at the transport layer and use non-200 API error responses. Always inspect both status and GraphQL errors. ## Machine reference - [factQuery browser reference](/reference/graphql/fact-query) - [GraphQL SDL](/schema.graphql) - [SDL usage guide](/tools/graphql-sdl) - `x-api-version` returns the date-based GraphQL schema identity The application may expose GraphiQL in development, but integrations must not depend on a production playground. --- # Run your first warehouse query > Obtain a real data-model slug, send a bounded factQuery with variables, and verify empty or populated GraphQL results. Obtain a real data-model slug through JSON:API, then send a bounded GraphQL query. A model with no persisted facts returns an empty `results` array; that is a valid query but not proof that a workflow has written records. ## What you will do 1. List the visible data models and choose one real slug. 2. Write a GraphQL query and variables to a temporary JSON request file. 3. Request at most ten latest rows. 4. Verify the GraphQL data/error shape and version headers. ## Prerequisites - Create a model through the Warehouse product flow described in [Data models](/help/data-dashboards/data-models). - For a populated result, run a workflow whose harmonizer persists facts for that model. - Use a service account in the same workspace with `api_read` and an active run-as member. - Set `HODOFLOW_BASE_URL` and `TOKEN`; install cURL and `jq`. Do not invent a slug from a display name. The stored slug is the GraphQL table identity. ## Request List the visible data models. This collection currently does not expose a pagination parameter, so it is the documented exception to the portal's explicit-limit rule. Use the response only to select a real slug: ```bash MODELS_RESPONSE="$( curl --silent --show-error --globoff \ --header "Accept: application/vnd.api+json" \ --header "Authorization: Bearer $TOKEN" \ "$HODOFLOW_BASE_URL/api/json/v1/data_models?fields[data_model]=name,slug" )" printf '%s\n' "$MODELS_RESPONSE" | jq '.data[] | {name: .attributes.name, slug: .attributes.slug}' DATA_MODEL_SLUG="$(printf '%s\n' "$MODELS_RESPONSE" | jq -er '.data[0].attributes.slug')" ``` Create a request file. Query and variables stay separate, which avoids escaping nested values by hand: ```bash QUERY_FILE="$(mktemp)" jq -n --arg slug "$DATA_MODEL_SLUG" '{ query: "query FirstFacts($slug: String!, $limit: Int!) { factQuery(dataModelSlug: $slug, mode: \"latest\", limit: $limit) { count hasNextPage results { entityId observedAt data } } }", variables: {slug: $slug, limit: 10} }' > "$QUERY_FILE" curl --silent --show-error --include \ --request POST \ --header "Content-Type: application/json" \ --header "Authorization: Bearer $TOKEN" \ --data-binary "@$QUERY_FILE" \ "$HODOFLOW_BASE_URL/api/graphql" ``` You should see `200`, a date-based `x-api-version`, `x-request-id`, and either a `data.factQuery` object or a GraphQL `errors` array explaining why resolution did not succeed. ## Expected response A valid model with no matching facts returns: ```json { "data": { "factQuery": { "count": 0, "hasNextPage": false, "results": [] } } } ``` A populated result contains at most ten `Fact` rows: ```json { "data": { "factQuery": { "count": 1, "hasNextPage": false, "results": [ { "entityId": "order-123", "observedAt": "2026-07-31T18:30:00Z", "data": { "status": "shipped", "total": 125.5 } } ] } } } ``` Field names inside `data` come from your model; they are not a fixed schema in this guide. ## Troubleshooting - Empty model list: create a data model in the same workspace first. - Unknown slug error: re-read `.attributes.slug`; names and slugs differ. - `401`: exchange once and retry once. - `403`: `factQuery` requires `api_read` and an active run-as member. - Complexity error: reduce `limit` or selected fields. - Empty `results`: confirm a workflow persisted facts for this exact model and workspace. An empty result is not an authentication failure. - `429`: obey `Retry-After` before retrying. Remove the temporary file and local response values: ```bash rm "$QUERY_FILE" unset QUERY_FILE MODELS_RESPONSE DATA_MODEL_SLUG ``` ## Next steps - Compare [latest and timeline semantics](/graphql/fact-query). - Add variables-based [filters and pagination](/graphql/filters-pagination-limits). - Browse the exact [factQuery reference](/reference/graphql/fact-query). --- # factQuery arguments and results > Understand latest versus timeline semantics and every public factQuery argument, PageOfFact field, and Fact field. `factQuery` reads one data model as a logical table. The model slug selects the table, its fields define allowed payload columns, and each `Fact` is one mapped record. It requires an authenticated service account with `api_read`. ## Operation signature ```graphql factQuery( dataModelSlug: String! mode: String timeAxis: String since: DateTime until: DateTime filters: String sort: String sortDirection: String limit: Int offset: Int ): PageOfFact ``` | Argument | Values | Default | Meaning | |---|---|---|---| | `dataModelSlug` | required string | — | Model resolved in the credential workspace | | `mode` | `latest`, `timeline` | `latest` | Current row per entity or history | | `timeAxis` | `observed_at`, `inserted_at` | `observed_at` | Timestamp used for the window | | `since` | ISO-8601 DateTime | mode-dependent | Inclusive lower bound | | `until` | ISO-8601 DateTime | now in timeline | Upper bound | | `filters` | JSON array encoded as string | none | At most 20 field/operator/value clauses | | `sort` | `observed_at`, `inserted_at`, `entity_id` | `observed_at` | Timeline ordering field | | `sortDirection` | `asc`, `desc` | `desc` | Timeline direction | | `limit` | integer, maximum 250 | `50` | Page size, also complexity-sensitive | | `offset` | non-negative integer | `0` | Rows to skip | Unknown enumerated values are errors, not silent fallbacks. Pass `limit` explicitly even though the runtime defaults to 50. ## Latest mode `latest` returns one most recent row for each non-null `entityId`. It is useful for current-state views. Rows without entity identity are excluded, and clients must not depend on server ordering from the distinct-per-entity query. Latest mode is not automatically time-clamped because a current row may be old. Any explicit `since` or `until` still applies as a normal filter. ## Timeline mode `timeline` returns every matching historical row. Sort defaults to newest `observed_at` first. If no window is supplied, it uses the last 90 days ending now. A requested span wider than 366 days is clamped forward to 366 days. Always provide a bounded page size and, for repeatable exports, explicit time bounds. Offset pagination is supported; keyset pagination is not. ## PageOfFact The result page exposes: - `results` — selected `Fact` rows; - `count` — total matching count when requested; - `hasNextPage` and `hasPreviousPage`; - `pageNumber` and `lastPage`; and - `limit` — applied page size. Requesting `count` can require an additional count query. Ask for it only when the client uses it. ## Fact Public fields include `id`, `dataModelId`, `entityId`, `observedAt`, `insertedAt`, and `data`. The `data` JSON object contains values mapped to the model's public fields. Lineage and internal execution fields remain outside this logical-table response unless explicitly present in the current [GraphQL SDL](/schema.graphql). ## Errors Bad query syntax/types, unknown model slugs, invalid filters, and cost-limit violations appear in GraphQL's `errors` array, often with HTTP `200`. Invalid tokens and `429` rate limits use transport-level API errors. Keep `x-request-id` for both. Use the browsable [factQuery endpoint reference](/reference/graphql/fact-query) for copyable query/variables examples. --- # GraphQL filters, pagination, and limits > Encode factQuery filters safely, page with explicit limits and offsets, and stay within time-window, complexity, depth, and token guards. `factQuery` filters are a JSON document carried inside one GraphQL string. Always pass them as a variable so your JSON encoder handles the nested escaping. ## Valid filter variables ```graphql query ShippedOrders($slug: String!, $filters: String!, $offset: Int!) { factQuery( dataModelSlug: $slug mode: "timeline" filters: $filters since: "2026-07-01T00:00:00Z" limit: 20 offset: $offset ) { hasNextPage results { entityId observedAt data } } } ``` ```json { "slug": "order", "offset": 0, "filters": "[{\"field\":\"status\",\"op\":\"eq\",\"value\":\"shipped\"},{\"field\":\"total\",\"op\":\"gte\",\"value\":100}]" } ``` Each clause has `field`, `op`, and non-null `value`. `field` must exactly match one DataModelField and at most 20 clauses are accepted. | Field type | Operators | |---|---| | String | `eq`, `neq`, `contains` | | Integer/Float | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | | Datetime | `eq`, `neq`, `gt`, `gte`, `lt`, `lte` | | Boolean | `eq`, `neq` | `contains` is a case-insensitive literal substring. `%` and `_` in the value are escaped instead of becoming SQL wildcards. ## Invalid filters These are query errors and must be corrected before retrying: - malformed JSON in the GraphQL string; - unknown or non-filterable field names; - an operator unsupported by the model field type; - values that cannot cast to the field type; - `null` values; or - more than 20 clauses. The error identifies the invalid field/operator where safe. It does not expose arbitrary warehouse payload paths. ## Offset pagination `limit` defaults to 50, has a hard maximum of 250, and should still be explicit. Start `offset` at zero and add the applied limit while `hasNextPage` is true. Request `count` on the first page only when needed. The 250 maximum is not a throughput promise: complexity often binds first. ## Complexity The default maximum query complexity is 200. A page field selection is multiplied by the requested limit, so narrower selections permit more rows. A query selecting several page fields and `results { entityId observedAt data }` can exceed the limit at `limit: 50`; reduce the limit or selected fields. Do not request fields “just in case.” GraphQL's explicit selection is a cost and data-minimization tool. ## Depth, document size, and windows Default configurable guards are depth 10 and 8,000 parseable tokens. Violations are rejected before resolution. Timeline windows default to 90 days and clamp at 366 days; explicitly bounded smaller windows are easier to operate. Per-credential rate limiting also applies: 300 requests per 60 seconds by default, with `429` and `Retry-After` on breach. For exact types and response objects, open the [factQuery reference](/reference/graphql/fact-query). --- # Webhook ingestion > Send authenticated inbound deliveries to saved, active webhook workflows and follow the runs they create. Webhook ingestion turns one authenticated inbound `POST` into one workflow run. It is for sending events **into** Hodoflow; it is not an outbound event-webhook product. ```text POST /api/v1/webhooks/:token ``` The token comes from a saved webhook trigger. It resolves one active trigger in one organization and must be treated as a secret. The delivery must also carry a bearer token minted from the **exact service account selected on that trigger**. That account needs the `webhooks` scope. ## Before the first delivery Webhook ingestion needs product state that the JSON:API quickstart does not: 1. create a service account with `webhooks` in the intended workspace; 2. create a workflow with a webhook trigger and select that service account; 3. add at least one valid workflow step, save the workflow, and activate it; 4. copy the token-only URL shown on the active workflow; and 5. exchange the selected account's client credentials for a short-lived bearer token. Follow [Send your first webhook delivery](/webhook-ingestion/first-delivery) for the complete setup, request, accepted response, and run lookup. ## Delivery contract Send JSON and a stable delivery identifier: ```bash curl --silent --show-error \ --request POST \ --header "Authorization: Bearer $TOKEN" \ --header "Content-Type: application/json" \ --header "Idempotency-Key: $DELIVERY_ID" \ --data-binary @delivery.json \ "$HODOFLOW_BASE_URL/api/v1/webhooks/$TRIGGER_TOKEN" ``` An accepted delivery returns `202`, but the workflow has not necessarily finished: ```json { "status": "accepted", "workflow_run_id": "550e8400-e29b-41d4-a716-446655440000" } ``` Use the returned ID to read the run through JSON:API. A repeated delivery with the same stable identifier returns `202` and `{"status":"duplicate"}` without creating a second run. ## Payload behavior The JSON request becomes the workflow payload. If the top-level document has a `payload` member, only that member's value is used as the workflow payload; otherwise the whole document is used. The default body limit is 1 MiB. The default per-trigger rate is 100 requests in a 60-second fixed window. Deployments can configure both values, so clients must honor `413` and the `Retry-After` header on `429` rather than assuming the defaults. ## Security choices The trigger URL and bearer token are separate credentials. Possession of only one is insufficient. An attached inbound connector may add API-key or HMAC-SHA256 verification; Basic and Bearer connector modes are rejected because they conflict with the mandatory service-account `Authorization` header. Read [Authentication and signatures](/webhook-ingestion/authentication-signatures) before integrating a signed sender. Read [Deduplication, retries, and troubleshooting](/webhook-ingestion/retries-troubleshooting) before implementing retry behavior. ## Reference and next step - [Webhook endpoint reference](/reference/webhooks/ingest) - [Token exchange reference](/reference/authentication/token-exchange) - [Triggers and webhooks product guide](/help/workflows/triggers-and-webhooks) Next, [send a controlled first delivery](/webhook-ingestion/first-delivery) and verify the returned workflow run. --- # Send your first webhook delivery > Configure an active webhook workflow, send one idempotent JSON delivery, and verify the accepted run through JSON:API. Create one controlled delivery and follow it from `202 Accepted` to the JSON:API run record. The selected service account needs `webhooks` for delivery and `api_read` for the verification request. ## What you will do 1. Save and activate a workflow with a webhook trigger. 2. Exchange the selected service account's credentials. 3. Send one JSON body with a stable `Idempotency-Key`. 4. Capture the `workflow_run_id` from the `202` response. 5. Read that run through JSON:API. ## Prerequisites - In the intended workspace, create a service account with `webhooks` and `api_read`, and select an active run-as member. - Create a workflow, choose a **Webhook** trigger, and select that exact service account. Add at least one valid step, save, and activate the workflow. - Copy the token-only URL displayed on the active workflow. Do not use the builder's `/test` endpoint: it only previews a payload and creates no run. - Collect the selected account's client ID and client secret. Set `HODOFLOW_BASE_URL` as described in [Base URLs, workspaces, and versions](/get-started/base-urls-workspaces-versions). - Use Bash, cURL, and `jq` on a trusted machine. The trigger token is secret material. Do not put it or the bearer token in source control, logs, screenshots, browser storage, or support messages. ## Request Read the trigger token and service-account credentials interactively: ```bash read -r -s -p "Webhook trigger token: " TRIGGER_TOKEN printf '\n' read -r -p "Client ID: " CLIENT_ID read -r -s -p "Client secret: " CLIENT_SECRET printf '\n' export TRIGGER_TOKEN CLIENT_ID CLIENT_SECRET ``` Exchange the client credentials. There is no refresh token: ```bash TOKEN_RESPONSE="$( jq -n '{ grant_type: "client_credentials", client_id: env.CLIENT_ID, client_secret: env.CLIENT_SECRET }' | curl --silent --show-error \ --request POST \ --header "Content-Type: application/json" \ --data-binary @- \ "$HODOFLOW_BASE_URL/api/v1/oauth/token" )" TOKEN="$(printf '%s\n' "$TOKEN_RESPONSE" | jq -er '.access_token')" ``` Create a payload file and a sender-stable delivery ID. Reuse this ID only for retries of this exact delivery: ```bash DELIVERY_FILE="$(mktemp)" DELIVERY_ID="first-delivery-$(date -u +%Y%m%dT%H%M%SZ)" printf '%s\n' '{"event":"order.created","data":{"id":"order-123","total":125.5}}' > "$DELIVERY_FILE" DELIVERY_RESPONSE="$( curl --silent --show-error \ --request POST \ --header "Authorization: Bearer $TOKEN" \ --header "Content-Type: application/json" \ --header "Idempotency-Key: $DELIVERY_ID" \ --data-binary "@$DELIVERY_FILE" \ "$HODOFLOW_BASE_URL/api/v1/webhooks/$TRIGGER_TOKEN" )" printf '%s\n' "$DELIVERY_RESPONSE" | jq . RUN_ID="$(printf '%s\n' "$DELIVERY_RESPONSE" | jq -er '.workflow_run_id')" ``` The request is intentionally one delivery, so there is no collection limit to set. `202` means the run was durably accepted and enqueued, not completed. Read the returned run ID: ```bash curl --silent --show-error --include \ --header "Accept: application/vnd.api+json" \ --header "Authorization: Bearer $TOKEN" \ "$HODOFLOW_BASE_URL/api/json/v1/workflow_runs/$RUN_ID" ``` ## Expected response The delivery response is: ```json { "status": "accepted", "workflow_run_id": "550e8400-e29b-41d4-a716-446655440000" } ``` The lookup returns `200`, `x-api-version: v1`, an `x-request-id`, and one JSON:API workflow-run resource. Its state may still be `pending` or `running`: ```json { "data": { "type": "workflow_run", "id": "550e8400-e29b-41d4-a716-446655440000", "attributes": { "state": "pending" } } } ``` Poll deliberately rather than in a tight loop. Stop when the state becomes `completed`, `failed`, or `suspended`; see [Follow a workflow run](/guides/follow-workflow-run) for the full sequence. ## Troubleshooting - `401` with `{"error":"missing_credentials"}`: the bearer token is absent, invalid, or expired. Exchange once and retry once. - `401` with `{"error":"Unauthorized"}`: the token was not issued for the exact selected service account, the organization differs, or connector auth failed. Correct configuration before retrying. - `403` with `{"error":"Insufficient scope"}`: grant `webhooks` to the selected account; retrying the same token cannot add a scope. - `404` with `{"error":"Trigger not found"}`: verify the token-only URL and that the workflow is saved and active. - `413`: reduce the body before retrying. - `429` or `503`: wait `Retry-After`, then retry with the same delivery ID. - A retry returns `{"status":"duplicate"}`: the first attempt was accepted; no second run was created, so continue with the run ID captured previously. Clean up local secrets and the temporary body: ```bash rm "$DELIVERY_FILE" unset CLIENT_ID CLIENT_SECRET TOKEN TOKEN_RESPONSE TRIGGER_TOKEN DELIVERY_ID DELIVERY_FILE DELIVERY_RESPONSE RUN_ID ``` ## Next steps - Add sender verification with [API keys or HMAC signatures](/webhook-ingestion/authentication-signatures). - Implement [deduplication-aware retries](/webhook-ingestion/retries-troubleshooting). - Review the exact [webhook endpoint contract](/reference/webhooks/ingest). --- # Webhook authentication and signatures > Combine the trigger URL, selected service-account bearer token, and optional API-key or raw-body HMAC verification safely. Every production delivery has two mandatory credentials: the secret trigger token in the URL and a bearer token issued for the exact service account selected on that trigger. An optional inbound connector can add a third check. ## Mandatory service-account authentication The selected service account needs the `webhooks` scope and an active run-as member in the trigger's organization. Exchange its client credentials and send: ```text Authorization: Bearer ``` Using a different same-organization account is rejected with `401`. An expired or invalid token is also rejected. Deactivating the workflow immediately makes the trigger stop resolving and returns `404`; rotating a service-account secret prevents future exchanges but does not revoke an already minted token before its expiry. ## Optional API-key verification Attach an inbound connector using **API key** and configure a dedicated header, for example `x-provider-key`. Send it in addition to the bearer token: ```bash curl --request POST \ --header "Authorization: Bearer $TOKEN" \ --header "Content-Type: application/json" \ --header "x-provider-key: $PROVIDER_KEY" \ --data-binary @delivery.json \ "$HODOFLOW_BASE_URL/api/v1/webhooks/$TRIGGER_TOKEN" ``` The configured value is compared in constant time. A missing or empty secret fails closed. ## Optional HMAC-SHA256 verification For an HMAC connector, compute lowercase hexadecimal HMAC-SHA256 over the **exact raw request bytes**. The default signature header is `x-signature`, but the connector can configure another name. ```bash SIGNATURE="$(openssl dgst -sha256 -hmac "$WEBHOOK_SIGNING_SECRET" -hex delivery.json | awk '{print $2}')" curl --request POST \ --header "Authorization: Bearer $TOKEN" \ --header "Content-Type: application/json" \ --header "x-signature: $SIGNATURE" \ --data-binary @delivery.json \ "$HODOFLOW_BASE_URL/api/v1/webhooks/$TRIGGER_TOKEN" ``` Do not parse and reserialize the JSON between signing and sending: whitespace, key order, and the final newline change the bytes and therefore the digest. `--data-binary` preserves the file bytes. Compute the signature again only when the body changes; an unchanged retry uses the same signature and delivery ID. ## Unsupported connector modes Basic and Bearer connector authentication cannot be used by webhook triggers. Both need the `Authorization` header already reserved for the mandatory service-account token. A legacy trigger configured that way fails with `422` and `{"error":"Unsupported trigger authentication"}`; update the connector to API key or HMAC before retrying. All authentication failures intentionally use a small response surface: ```json {"error":"Unauthorized"} ``` The response does not reveal whether the service account, connector value, or signature was wrong. Use the request ID and your own redacted delivery logs to correlate attempts; never log the trigger token, bearer token, API key, signing secret, or full `Authorization` header. ## Next steps - Send a [controlled first delivery](/webhook-ingestion/first-delivery). - Design [stable delivery IDs and retry behavior](/webhook-ingestion/retries-troubleshooting). - Rotate credentials using [Rotation and revocation](/authentication/rotation-and-revocation). --- # Webhook deduplication, retries, and troubleshooting > Choose stable delivery IDs, interpret accepted duplicates, and retry only transient webhook-ingestion failures. Webhook senders must treat an unobserved `202` as ambiguous and retry safely. Hodoflow deduplicates a stable delivery identity per organization, so a retry can be acknowledged without starting the workflow twice. ## Choose one stable delivery ID The first non-empty header in this order becomes the delivery identity: 1. `Idempotency-Key` 2. `X-GitHub-Delivery` 3. `X-Shopify-Webhook-Id` 4. `X-Twilio-Idempotency-Token` Prefer `Idempotency-Key` for a custom sender. Generate it once for a logical event, persist it with the outbound job, and reuse it across every retry of that event. Do not use `X-Request-Id`: it identifies one attempt and changes on retry. Without one of these headers, Hodoflow hashes the trigger token and raw body in a two-hour time bucket. Byte-identical attempts in the bucket deduplicate, but a retry crossing the bucket boundary can run twice. The explicit header is the reliable choice. Delivery guard rows are retained for seven days by default. ## Accepted and duplicate responses The first successful delivery returns: ```json {"status":"accepted","workflow_run_id":"550e8400-e29b-41d4-a716-446655440000"} ``` A recognized retry returns the same `202` status but no new run ID: ```json {"status":"duplicate"} ``` `duplicate` means no second run was created. Keep the run ID from the first observed accepted response. If that response was lost, reconcile using your business identifier and workflow-run observability rather than sending a new delivery ID. ## Retry matrix | Status | Runtime body | Action | |---|---|---| | `202` | `accepted` or `duplicate` | Stop delivery retries | | `401` | `missing_credentials` | Exchange once if expired, then retry once with the same delivery ID | | `401` | `Unauthorized` | Fix selected-account, organization, or connector authentication | | `403` | `Insufficient scope` | Grant `webhooks`; do not retry unchanged | | `404` | `Trigger not found` | Confirm token and active workflow; do not retry unchanged | | `413` | `Request body too large` | Reduce the body; a byte-identical retry cannot succeed | | `422` | `Processing failed` or `Unsupported trigger authentication` | Fix payload or trigger configuration | | `429` | `Rate limit exceeded` | Wait the response's `Retry-After`, then retry with the same ID | | `503` | `Processing temporarily unavailable` | Wait `Retry-After`, back off, and retry with the same ID | Use exponential backoff with jitter for `503`, cap the attempt count and total elapsed time, and preserve the delivery ID. For `429`, the server's `Retry-After` is authoritative. Never turn a permanent `401`, `403`, `404`, `413`, or `422` into an infinite retry loop. ## Limits and checkpoints The default maximum body is 1,048,576 bytes and the default rate is 100 requests per trigger in a 60-second fixed window. Both are deployment-configurable. Check status, `Retry-After`, and the response body on every attempt. An accepted response means the run was committed and enqueued, not completed. Read `/api/json/v1/workflow_runs/:id` using `api_read`, then follow the run state as described in [Follow a workflow run](/guides/follow-workflow-run). ## Diagnose without exposing credentials Record a local sender attempt ID, stable delivery ID, response status, `x-request-id` when present, and timestamp. Redact URL path tokens and every credential. When the trigger returns `404`, confirm the workflow remains active; deactivation deliberately makes a real token indistinguishable from an unknown one. The legacy organization-qualified path still routes but is deprecated: ```text POST /api/v1/webhooks/:org_domain/:token ``` Use the token-only path. It avoids coupling the sender to an organization-domain rename. ## Next steps - Verify the [webhook operation reference](/reference/webhooks/ingest). - Add [HMAC verification](/webhook-ingestion/authentication-signatures). - Rehearse an unobserved-`202` retry in a non-production workspace and confirm the second response is `duplicate`. --- # Tools and downloads > Download the public OpenAPI and GraphQL contracts, browse the raw API viewer, or consume the Markdown and LLM indexes. Use these stable, release-matched artifacts to inspect the public API or feed your own documentation and code-generation workflow. The contracts describe the application host; the developer portal does not execute authenticated requests and never asks you to paste a credential. ## API specifications - [OpenAPI contract](/tools/openapi) — download and interpret the JSON:API, token-exchange, and webhook-ingestion operations exposed by this release. The raw document is available at [openapi.json](/openapi.json). - [GraphQL SDL contract](/tools/graphql-sdl) — generate types and inspect the public query schema without mistaking SDL for the runtime authorization and limit contract. The raw schema is available at [schema.graphql](/schema.graphql). - [Curated API reference](/reference) — purpose, access, parameters, examples, responses, errors, and related guides for every public operation. - [Raw Swagger viewer](/tools/swagger) — a secondary, non-executing view of the same OpenAPI document. OpenAPI covers the generated JSON:API transport plus reviewed manual definitions for token exchange and webhook ingestion. GraphQL has its own SDL. Start with the contract page for a surface before feeding its artifact to a generator. ## Markdown and LLM indexes - [Developer documentation index](/llms.txt) — links and descriptions for every canonical developer page. - [Full developer documentation](/llms-full.txt) — the same corpus in one text response. - Append `.md` to any canonical article URL for that page's Markdown alternate. The HTML pages, Markdown alternates, and indexes share one compiled source corpus. Legacy article aliases redirect to their canonical HTML or Markdown URL before content is served. --- # OpenAPI contract > Download and use Hodoflow's release-matched HTTP contract for JSON:API, token exchange, and webhook ingestion. The public [OpenAPI 3.0 document](/openapi.json) is the machine-readable contract for Hodoflow's generated **JSON:API** surface plus reviewed manual definitions for token exchange and webhook ingestion. Use it to discover paths, inspect request and response schemas, configure a raw viewer, or generate a client inside your own build. It is public and contains no tenant data or credentials. It is not a catalog of every HTTP route Hodoflow serves. Read the coverage boundary before treating it as an application-wide API definition. ## What the document covers | Surface | In `openapi.json`? | Authoritative companion | |---|---:|---| | JSON:API at `/api/json/v1` | Yes | [JSON:API conventions](/platform/json-api) | | GraphQL at `/api/graphql` | No | [GraphQL SDL contract](/tools/graphql-sdl) | | Token exchange at `/api/v1/oauth/token` | Yes | [Authentication](/authentication) | | Webhook ingestion at `/api/v1/webhooks/:token` | Yes | [Webhook ingestion](/webhook-ingestion) | Every path in the document is already absolute from the application origin: generated resource paths include `/api/json/v1`, while the reviewed manual paths include `/api/v1`. The public developer-host copy sets `servers[0].url` to the application origin, not to the developer portal and not to an origin with another API prefix. Join the server and path exactly once: ```text servers[0].url https://hodoflow.com path /api/json/v1/workflows request URL https://hodoflow.com/api/json/v1/workflows ``` Do not append `/api/json/v1` to the server URL before passing the document to a viewer or generator; doing so creates a doubled path. ## Download the release-matched document Download the document from the same release as the portal: ```bash export HODOFLOW_DEVELOPER_URL=https://developer.hodoflow.com curl -fsS "$HODOFLOW_DEVELOPER_URL/openapi.json" -o hodoflow-openapi.json ``` Check the advertised API origin and JSON:API contract version before using it: ```bash jq '{openapi, version: .info.version, servers}' hodoflow-openapi.json ``` A production document reports OpenAPI `3.0.0`, JSON:API contract version `v1`, and one application-origin server. The `v1` value identifies this API surface; it is not a release number and does not select the GraphQL schema version. The document served at `/openapi.json` is the recommended input for external tools because its server is explicit. The application also exposes a discovery copy at `/api/json/v1/open_api`, but a client fetching that copy already needs to know the application origin. ## Read an operation Paths map an HTTP method to its stable `operationId`, purpose, parameters, body, responses, errors, and product-facing tag. For example, inspect the list-workflows operation with: ```bash jq '.paths["/api/json/v1/workflows"].get' hodoflow-openapi.json ``` Interpret the important pieces as follows: | OpenAPI location | Meaning in Hodoflow | |---|---| | `operationId`, `summary`, and `description` | Stable client name, concise action, and operation purpose | | `security` and `components.securitySchemes.bearerAuth` | Send the short-lived access token as `Authorization: Bearer …`; token exchange is the unauthenticated exception | | `x-hodoflow` | Surface, version, stability, exact scope, idempotency behavior, related guides, and reviewed errors | | `parameters` | Only the query and path parameters accepted by that operation | | `style: deepObject` | Serialize nested JSON:API parameters with brackets, such as `page[limit]=25` | | `requestBody.content.application/vnd.api+json` | The JSON:API write document accepted by the operation | | `responses` | Successful document schemas plus the shared error response | | `components.schemas` | Resource, filter, input, and supporting object shapes referenced by operations | The top-level bearer scheme applies to authenticated operations. For JSON:API, safe methods require `api_read` and unsafe methods require `api_write`; `x-hodoflow.scope` carries that exact decision. Webhook ingestion requires `webhooks`. Token exchange has no bearer requirement because it produces the bearer token. Parameters are operation-specific. If `page`, a filter field, or a sort field does not appear on an operation, do not send it. In particular, do not assume that every collection paginates or that omission implies a default page size. See [JSON:API conventions](/platform/json-api) for media types, filter syntax, idempotency, response headers, and the error envelope. ## Generate a client safely Hodoflow does not currently publish an official SDK. A client generated from this document is your build artifact, so pin the downloaded input and review the generator output before shipping it. Use a generator that supports all of these OpenAPI 3.0 features: - `deepObject` query serialization for `filter`, `page`, and sparse fieldsets; - `application/vnd.api+json` request and response bodies; - referenced schemas, nullable unions, enums, patterns, and numeric bounds; - distinct create and update input schemas; and - response bodies whose top-level JSON:API shape is `{data, included, meta}`. Every public HTTP operation has a unique stable `operationId` and reviewed summary. Still wrap generated client methods behind names you control: generator naming and serialization choices remain build-tool behavior rather than part of Hodoflow's runtime contract. Also verify how your generator handles recursive filter objects and `additionalProperties`. A generated method that flattens a deep-object filter into an ordinary string will not send the same request the schema describes. Compare one generated collection request with the bracketed examples in the [first API request](/get-started/first-api-request) before adopting it broadly. Generating from this file can create helpers for token exchange and webhook ingestion. GraphQL remains outside OpenAPI and uses the [GraphQL SDL](/schema.graphql). Do not move either manual HTTP endpoint under the JSON:API `v1` base path. ## Detect contract changes Keep the exact OpenAPI file used to generate a released client. On upgrade, download the next file, normalize both documents, and review the diff before regenerating: ```bash jq -S . hodoflow-openapi.json > hodoflow-openapi.sorted.json jq -S . hodoflow-openapi.next.json > hodoflow-openapi.next.sorted.json diff -u hodoflow-openapi.sorted.json hodoflow-openapi.next.sorted.json ``` Review path and method removals, newly required inputs, narrower enums or bounds, schema type changes, and response-shape changes first. Additive paths, optional fields, and response properties can still affect strict decoders, so exercise the generated client against representative responses after every regeneration. At runtime, record `x-api-version` and `x-request-id` from responses. The first confirms which JSON:API contract served the request; the second is the correlation identifier to retain when diagnosing a mismatch with support. ## Choose the matching human reference Use the [curated API reference](/reference) for a browsable rendering with guidance, or the secondary [raw Swagger viewer](/tools/swagger) to inspect the document directly. Use [JSON:API conventions](/platform/json-api) for behavior shared across resource operations, and the task guides for a complete request, expected response, permissions, and recovery steps. The machine document defines the wire shape; the human pages explain how to use it safely. --- # GraphQL SDL contract > Download Hodoflow's versioned GraphQL schema and use it for validation and type generation with the runtime rules kept in view. The public [GraphQL schema definition](/schema.graphql) is the machine-readable type contract for `POST /api/graphql`. Use it to validate operations, power editor completion, or generate types for queries your integration owns. It is generated from the running schema and contains no tenant data or credentials. SDL describes GraphQL names and types. It does not describe every runtime rule that decides whether a query is authorized, affordable, or valid for a particular data model. Use it together with the [GraphQL guide](/graphql), not as a replacement for that guide. ## Download the schema Download the schema from the release-matched developer portal: ```bash export HODOFLOW_DEVELOPER_URL=https://developer.hodoflow.com curl -fsS "$HODOFLOW_DEVELOPER_URL/schema.graphql" -o hodoflow-schema.graphql ``` The first comment line identifies the contract version: ```graphql # Hodoflow GraphQL API — schema version YYYY-MM-DD ``` The next comment records that the file is generated from Hodoflow's running schema and must not be edited by hand. The date is the GraphQL schema version. It is independent of JSON:API `v1` and of the `x-api-version` header returned by the HTTP API. There is no request parameter that selects an older SDL: pin the file used by your integration and review a newer schema before upgrading. The endpoint serves SDL as `text/plain`, so it can be read directly in a browser or fetched by build tooling without authentication. Fetching the schema does not grant access to warehouse records. ## Read the public type system The schema currently has one root type: ```graphql schema { query: RootQueryType } ``` `RootQueryType` exposes two fields: | Field | Purpose | Authentication | |---|---|---| | `health` | Lightweight liveness result | Public | | `factQuery` | Read current or historical warehouse records for one data-model slug | Bearer token with `api_read` | There is no mutation root and no subscription root. The application may expose a GraphQL socket transport, but an SDL without a `subscription` root has no subscription field to call. Check the schema rather than generating a subscription client from the existence of a socket URL. `factQuery` returns `PageOfFact`. Generate against the nullability written in the SDL: the page itself and its `results` list are nullable, each returned `Fact` is non-null, and `count` is optional. A `Fact` includes identifiers and timestamps plus a `Json` `data` value whose properties come from the selected data model. Map the custom scalars deliberately: | Scalar | Wire value | Suggested client representation | |---|---|---| | `DateTime` | ISO-8601 timestamp normalized to UTC | Your language's offset-aware timestamp type, or a validated string | | `Json` | Arbitrary JSON value | A JSON value/object type rather than an unparsed GraphQL string | The `data` object is intentionally not expanded into a static GraphQL object type. Its keys and value shapes are defined by the data model in the workspace, so validate or decode them with the model contract your integration expects. ## Know what SDL cannot express here Several `factQuery` arguments are currently `String` or `Int` in SDL even though the resolver applies narrower rules. The SDL therefore cannot tell a generic generator all of the following: - `mode` accepts `latest` or `timeline` and defaults to `latest`; - `timeAxis` accepts `observed_at` or `inserted_at` and defaults to `observed_at`; - `sort` and `sortDirection` accept a defined set of values; - `limit` defaults to 50 and is capped at 250 before query-cost limits apply; - `offset` defaults to 0; - `filters` is a JSON array encoded inside a GraphQL string, with field-type specific operators and a maximum of 20 entries; - omitted timeline bounds create a 90-day window, capped at 366 days; and - complexity, nesting-depth, token, and per-credential rate limits apply before or during execution. The exact values, paging rules, cost formula, and error examples are maintained in [GraphQL](/graphql). Authentication is also outside GraphQL's type system: send `Authorization: Bearer $TOKEN` for `factQuery`, and give that service account `api_read`. A schema-aware client cannot infer those requirements from the field signature. ## Generate types from operations you own Prefer document-based generation: keep each named query in source control and generate its variables and result type against the pinned SDL. This produces a smaller, more useful contract than generating a general-purpose client for the entire schema. For example, a bounded operation can live in `current-orders.graphql`: ```graphql query CurrentOrders($slug: String!, $limit: Int!) { factQuery(dataModelSlug: $slug, mode: "latest", limit: $limit) { hasNextPage results { entityId observedAt data } } } ``` Configure the generator's `DateTime` and `Json` mappings, then make the HTTP request yourself or through a standard GraphQL client. Generated types do not turn the result into a Hodoflow SDK, refresh access tokens, add the `api_read` scope, choose safe query limits, or decode the model-specific `data` object for you. Keep variables separate from the query text. This is especially important for `filters`, which is JSON nested inside a GraphQL string; passing it as a variable avoids another layer of manual escaping. ## Detect schema changes Store the exact SDL used by a released integration. Before adopting a newer release, compare schemas with a GraphQL schema-diff tool or a plain text diff: ```bash diff -u hodoflow-schema.graphql hodoflow-schema.next.graphql ``` Treat removed fields or arguments, tightened nullability, and changed scalar or field types as breaking. Additive fields remain safe only when your decoder ignores response keys it did not request—which ordinary GraphQL clients do, because a response follows the operation's selection set. A changed date comment tells you the schema artifact changed; it does not by itself say whether the change affects your operations. Revalidate every stored query against the new SDL, regenerate types, and run the integration's own query-level checks before deployment. For a complete request and operational behavior, continue to the [GraphQL guide](/graphql). For the other generated machine contract, see the [OpenAPI contract](/tools/openapi).