Skip to content

Spell out how API versioning works across the docs - #63

Merged
Adron merged 2 commits into
mainfrom
docs/versioning-review
Oct 22, 2026
Merged

Adron merged 2 commits into
mainfrom
docs/versioning-review

Conversation

@Adron

@Adron Adron commented Oct 22, 2026

Copy link
Copy Markdown
Member

Summary

A top-to-bottom review of the docs after #37 moved the API version into the Substratal-Version header. Most pages already described it correctly. This fixes the gaps:

  • OpenAPI Specification → Versioning and change policy still described versions as URL prefixes ("ship without a new prefix", "get a new prefix"). It now says version numbers.
  • What counts as a breaking change is now listed in NFR → Versioning: removing or renaming a field, endpoint, or parameter; changing a type or format; a new or newly required request field; a different status or error code for an existing condition; or a changed default or behavior.
  • Retirement wasn't specified. Deprecation (RFC 9745, not RFC 8594 as the docs said) and Sunset (RFC 8594) go on responses to requests that name the deprecated version. After the Sunset date, requests naming it get 400 unsupported_api_version. This is now stated in Conventions and NFR.
  • Two versions with similar names: the Substratal-Version request header and a webhook subscription's payload api_version date are independent. This is now said on Conventions and Webhooks, and both have Glossary entries.
  • Auth → POST /auth/oauth/token notes in the section itself that it doesn't need the header, and why.
  • Changelog entry.

Docs only: no API behavior changes. A local site build checked 2,415 in-site links and found no broken anchors.

🤖 Generated with Claude Code

Adron and others added 2 commits October 21, 2026 20:21
A top-to-bottom review after moving the version into the
Substratal-Version header:

- NFR: what counts as a breaking change; Deprecation (RFC 9745) and
  Sunset (RFC 8594) go on responses to requests naming the deprecated
  version; after Sunset, that version is 400 unsupported_api_version
- Conventions: the same retirement rule, and that a webhook
  subscription's payload api_version is a separate version
- Webhooks -> Payload versions: independent of Substratal-Version
- OpenAPI Specification: versions are numbers, not URL prefixes
- Auth -> POST /auth/oauth/token: no version header needed, and why
- Glossary: API version and Payload version
- Changelog entry

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tools.json embeds the Glossary as an MCP resource, so gen-check failed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

terraform plan (production)

This is exactly what merging will change in AWS.

Plan
random_password.mcp_session: Refreshing state... [id=none]
aws_kms_key.data: Refreshing state... [id=b0cda5a8-651e-419f-b8ab-61772bce79da]
data.aws_servicequotas_service_quota.lambda_concurrency: Reading...
data.aws_route53_zone.main: Reading...
aws_iam_role.scheduler: Refreshing state... [id=substratal-prod-scheduler]
aws_sns_topic.alarms: Refreshing state... [id=arn:aws:sns:us-east-1:196936048600:substratal-prod-alarms]
data.aws_caller_identity.me: Reading...
module.webhook_worker.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-webhook-worker]
module.jobs.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-jobs]
aws_cloudwatch_log_group.api_access: Refreshing state... [id=/aws/apigateway/substratal-prod]
module.api.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-api]
data.aws_caller_identity.me: Read complete after 0s [id=196936048600]
module.webhook_worker.aws_iam_role.this: Refreshing state... [id=substratal-prod-webhook-worker]
data.aws_servicequotas_service_quota.lambda_concurrency: Read complete after 1s [id=arn:aws:servicequotas:us-east-1::lambda/L-B99A9384]
aws_apigatewayv2_api.main: Refreshing state... [id=rv9n7e3ww4]
aws_kms_key.signing: Refreshing state... [id=0d2770cd-bbc8-4904-a432-2e9c9304a3ca]
module.mcp.aws_iam_role.this: Refreshing state... [id=substratal-prod-mcp]
aws_acm_certificate.api: Refreshing state... [id=arn:aws:acm:us-east-1:196936048600:certificate/a8e2136f-64d3-456b-904a-05ea74eba9b3]
aws_sqs_queue.webhooks_dlq: Refreshing state... [id=https://sqs.us-east-1.amazonaws.com/196936048600/substratal-prod-webhooks-dlq]
aws_sesv2_email_identity.mail: Refreshing state... [id=mail.substratalapps.com]
module.stripe_webhook.aws_iam_role.this: Refreshing state... [id=substratal-prod-stripe-webhook]
data.aws_route53_zone.main: Read complete after 1s [id=Z0783627X8ON1GH6I0XB]
aws_secretsmanager_secret.app: Refreshing state... [id=arn:aws:secretsmanager:us-east-1:196936048600:secret:substratal-prod/app-1hXTGg]
module.jobs.aws_iam_role.this: Refreshing state... [id=substratal-prod-jobs]
module.stripe_webhook.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-stripe-webhook]
aws_rds_cluster.main: Refreshing state... [id=substratal-prod-db]
module.api.aws_iam_role.this: Refreshing state... [id=substratal-prod-api]
module.mcp.aws_cloudwatch_log_group.this: Refreshing state... [id=/aws/lambda/substratal-prod-mcp]
aws_s3_bucket.audit_archive: Refreshing state... [id=substratal-prod-audit-archive-196936048600]
aws_cloudwatch_metric_alarm.security["refresh-reuse"]: Refreshing state... [id=substratal-prod-security-refresh-reuse]
aws_cloudwatch_metric_alarm.security["destructive-blocked"]: Refreshing state... [id=substratal-prod-security-destructive-blocked]
aws_cloudwatch_metric_alarm.security["failed-logins"]: Refreshing state... [id=substratal-prod-security-failed-logins]
aws_kms_alias.data: Refreshing state... [id=alias/substratal-prod-data]
aws_route53_record.dmarc: Refreshing state... [id=Z0783627X8ON1GH6I0XB__dmarc.mail.substratalapps.com_TXT]
aws_cloudwatch_metric_alarm.api_5xx: Refreshing state... [id=substratal-prod-api-5xx]
aws_apigatewayv2_stage.default: Refreshing state... [id=$default]
aws_kms_alias.signing: Refreshing state... [id=alias/substratal-prod-jwt-signing]
aws_cloudwatch_metric_alarm.webhook_dlq: Refreshing state... [id=substratal-prod-webhooks-dlq]
aws_sqs_queue.webhooks: Refreshing state... [id=https://sqs.us-east-1.amazonaws.com/196936048600/substratal-prod-webhooks]
aws_route53_record.cert_validation["api.substratalapps.com"]: Refreshing state... [id=Z0783627X8ON1GH6I0XB__45a2e898c02bdc2297dde6c5e5a798a8.api.substratalapps.com._CNAME]
aws_route53_record.dkim[0]: Refreshing state... [id=Z0783627X8ON1GH6I0XB_fmy2g7whqti56v5vn56es6izxo4yzayc._domainkey.mail.substratalapps.com_CNAME]
aws_route53_record.dkim[1]: Refreshing state... [id=Z0783627X8ON1GH6I0XB_fhl63kaowzjfd2splohuyaifnnansx5d._domainkey.mail.substratalapps.com_CNAME]
aws_route53_record.dkim[2]: Refreshing state... [id=Z0783627X8ON1GH6I0XB_rp4dt5rjjkpnhdyxud6l2spetqo5nom4._domainkey.mail.substratalapps.com_CNAME]
aws_cloudwatch_log_metric_filter.security["refresh-reuse"]: Refreshing state... [id=substratal-prod-refresh-reuse]
aws_cloudwatch_log_metric_filter.security["destructive-blocked"]: Refreshing state... [id=substratal-prod-destructive-blocked]
aws_cloudwatch_log_metric_filter.security["failed-logins"]: Refreshing state... [id=substratal-prod-failed-logins]
module.webhook_worker.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-webhook-worker:logs]
module.mcp.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-mcp:logs]
terraform_data.app_secret_init: Refreshing state... [id=5474e6c3-4843-79d8-9346-9fc3d3f8fa87]
module.stripe_webhook.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-stripe-webhook:logs]
module.jobs.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-jobs:logs]
module.api.aws_iam_role_policy.logs: Refreshing state... [id=substratal-prod-api:logs]
aws_cloudwatch_metric_alarm.aurora_near_max: Refreshing state... [id=substratal-prod-aurora-acu-near-max]
aws_rds_cluster_instance.main: Refreshing state... [id=substratal-prod-db-1]
module.mcp.aws_lambda_function.this: Refreshing state... [id=substratal-prod-mcp]
module.stripe_webhook.aws_lambda_function.this: Refreshing state... [id=substratal-prod-stripe-webhook]
module.api.aws_iam_role_policy.this[0]: Refreshing state... [id=substratal-prod-api:function]
module.stripe_webhook.aws_iam_role_policy.this[0]: Refreshing state... [id=substratal-prod-stripe-webhook:function]
module.webhook_worker.aws_lambda_function.this: Refreshing state... [id=substratal-prod-webhook-worker]
module.webhook_worker.aws_iam_role_policy.this[0]: Refreshing state... [id=substratal-prod-webhook-worker:function]
module.api.aws_lambda_function.this: Refreshing state... [id=substratal-prod-api]
aws_acm_certificate_validation.api: Refreshing state... [id=2026-10-20 20:44:42.323 +0000 UTC]
aws_apigatewayv2_domain_name.api: Refreshing state... [id=api.substratalapps.com]
aws_apigatewayv2_api_mapping.api: Refreshing state... [id=zdmjg2]
aws_route53_record.api: Refreshing state... [id=Z0783627X8ON1GH6I0XB_api.substratalapps.com_A]
module.stripe_webhook.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-stripe-webhook:live]
module.api.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-api:live]
module.webhook_worker.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-webhook-worker:live]
aws_s3_bucket_public_access_block.audit_archive: Refreshing state... [id=substratal-prod-audit-archive-196936048600]
aws_s3_bucket_versioning.audit_archive: Refreshing state... [id=substratal-prod-audit-archive-196936048600]
aws_s3_bucket_server_side_encryption_configuration.audit_archive: Refreshing state... [id=substratal-prod-audit-archive-196936048600]
module.jobs.aws_iam_role_policy.this[0]: Refreshing state... [id=substratal-prod-jobs:function]
module.jobs.aws_lambda_function.this: Refreshing state... [id=substratal-prod-jobs]
module.mcp.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-mcp:live]
aws_cloudwatch_metric_alarm.lambda_throttles["api"]: Refreshing state... [id=substratal-prod-api-throttles]
aws_lambda_event_source_mapping.webhooks: Refreshing state... [id=bed6416f-6a72-474a-83e5-559c995cf8f3]
aws_cloudwatch_metric_alarm.lambda_throttles["worker"]: Refreshing state... [id=substratal-prod-worker-throttles]
aws_lambda_permission.apigw["stripe"]: Refreshing state... [id=apigateway-stripe]
aws_apigatewayv2_integration.fn["mcp"]: Refreshing state... [id=8xk20ia]
aws_apigatewayv2_integration.fn["api"]: Refreshing state... [id=3h3bk4c]
aws_apigatewayv2_integration.fn["stripe"]: Refreshing state... [id=34l24l9]
aws_lambda_permission.apigw["api"]: Refreshing state... [id=apigateway-api]
aws_lambda_permission.apigw["mcp"]: Refreshing state... [id=apigateway-mcp]
aws_apigatewayv2_route.r["POST /internal/stripe/webhook"]: Refreshing state... [id=bczysio]
aws_apigatewayv2_route.r["DELETE /mcp"]: Refreshing state... [id=peme2oj]
aws_apigatewayv2_route.r["GET /mcp"]: Refreshing state... [id=s6ez97n]
aws_apigatewayv2_route.r["POST /mcp"]: Refreshing state... [id=a197cl8]
aws_apigatewayv2_route.r["$default"]: Refreshing state... [id=cr82zzs]
aws_apigatewayv2_route.r["GET /.well-known/{proxy+}"]: Refreshing state... [id=kb35wrh]
module.jobs.aws_lambda_alias.live: Refreshing state... [id=arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-jobs:live]
aws_iam_role_policy.scheduler: Refreshing state... [id=substratal-prod-scheduler:terraform-339ca2ff2a164d299b046f72e0]
aws_scheduler_schedule.job["webhook-dispatch"]: Refreshing state... [id=default/substratal-prod-webhook-dispatch]
aws_scheduler_schedule.job["webhook-health"]: Refreshing state... [id=default/substratal-prod-webhook-health]
aws_scheduler_schedule.job["seat-sync"]: Refreshing state... [id=default/substratal-prod-seat-sync]
aws_scheduler_schedule.job["push-maintenance"]: Refreshing state... [id=default/substratal-prod-push-maintenance]
aws_cloudwatch_metric_alarm.lambda_errors["stripe"]: Refreshing state... [id=substratal-prod-stripe-errors]
aws_scheduler_schedule.job["audit-archive"]: Refreshing state... [id=default/substratal-prod-audit-archive]
aws_scheduler_schedule.job["entitlement-sweep"]: Refreshing state... [id=default/substratal-prod-entitlement-sweep]
aws_scheduler_schedule.job["cleanup"]: Refreshing state... [id=default/substratal-prod-cleanup]
aws_scheduler_schedule.job["erasure-cascade"]: Refreshing state... [id=default/substratal-prod-erasure-cascade]
aws_scheduler_schedule.job["stripe-events"]: Refreshing state... [id=default/substratal-prod-stripe-events]
aws_scheduler_schedule.job["subscription-access"]: Refreshing state... [id=default/substratal-prod-subscription-access]
aws_cloudwatch_metric_alarm.lambda_errors["worker"]: Refreshing state... [id=substratal-prod-worker-errors]
aws_scheduler_schedule.job["settings-projections"]: Refreshing state... [id=default/substratal-prod-settings-projections]
aws_cloudwatch_metric_alarm.lambda_errors["api"]: Refreshing state... [id=substratal-prod-api-errors]
aws_cloudwatch_metric_alarm.lambda_errors["jobs"]: Refreshing state... [id=substratal-prod-jobs-errors]
aws_cloudwatch_metric_alarm.lambda_errors["mcp"]: Refreshing state... [id=substratal-prod-mcp-errors]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.api.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "6" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-api:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.api.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-api"
      ~ last_modified                  = "2026-10-22T03:28:14.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-api:6" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-api:6/invocations" -> (known after apply)
      ~ s3_key                         = "1565cd9446d963529a59855cadff7010f8939e28/api.zip" -> "b7c0194a45fce5144e191002e062254859bee985/api.zip"
        tags                           = {}
      ~ version                        = "6" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.jobs.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "6" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-jobs:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.jobs.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-jobs"
      ~ last_modified                  = "2026-10-22T03:28:14.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-jobs:6" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-jobs:6/invocations" -> (known after apply)
      ~ s3_key                         = "1565cd9446d963529a59855cadff7010f8939e28/jobs.zip" -> "b7c0194a45fce5144e191002e062254859bee985/jobs.zip"
        tags                           = {}
      ~ version                        = "6" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.mcp.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "7" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-mcp:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.mcp.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-mcp"
      ~ last_modified                  = "2026-10-22T03:28:14.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-mcp:7" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-mcp:7/invocations" -> (known after apply)
      ~ s3_key                         = "1565cd9446d963529a59855cadff7010f8939e28/mcp.zip" -> "b7c0194a45fce5144e191002e062254859bee985/mcp.zip"
        tags                           = {}
      ~ version                        = "7" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.stripe_webhook.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "6" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-stripe-webhook:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.stripe_webhook.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-stripe-webhook"
      ~ last_modified                  = "2026-10-22T03:28:14.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-stripe-webhook:6" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-stripe-webhook:6/invocations" -> (known after apply)
      ~ s3_key                         = "1565cd9446d963529a59855cadff7010f8939e28/stripe-webhook.zip" -> "b7c0194a45fce5144e191002e062254859bee985/stripe-webhook.zip"
        tags                           = {}
      ~ version                        = "6" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.webhook_worker.aws_lambda_alias.live will be updated in-place
  ~ resource "aws_lambda_alias" "live" {
      ~ function_version = "6" -> (known after apply)
        id               = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-webhook-worker:live"
        name             = "live"
        # (5 unchanged attributes hidden)
    }

  # module.webhook_worker.aws_lambda_function.this will be updated in-place
  ~ resource "aws_lambda_function" "this" {
        id                             = "substratal-prod-webhook-worker"
      ~ last_modified                  = "2026-10-22T03:28:14.000+0000" -> (known after apply)
      ~ qualified_arn                  = "arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-webhook-worker:6" -> (known after apply)
      ~ qualified_invoke_arn           = "arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:196936048600:function:substratal-prod-webhook-worker:6/invocations" -> (known after apply)
      ~ s3_key                         = "1565cd9446d963529a59855cadff7010f8939e28/webhook-worker.zip" -> "b7c0194a45fce5144e191002e062254859bee985/webhook-worker.zip"
        tags                           = {}
      ~ version                        = "6" -> (known after apply)
        # (28 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

Plan: 0 to add, 10 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

@Adron
Adron merged commit 364ce5b into main Oct 22, 2026
4 checks passed
Adron added a commit that referenced this pull request Oct 22, 2026
Brings in the Substratal-Version header and root paths (#37), the
plan-job secret fix (#59), and the versioning docs (#63).

- Conflicts: plan-remaining.md keeps this branch's removal of the
  now-built section; Auth -> Lifetime keeps this branch's wording with
  root paths and anchors; the Changelog puts this branch's entry above
  main's two 2026-10-21 entries
- openapi.yaml 1.0.0-draft.5 (main took draft.4 for the header change)
- test_user_expiry_test.go: root paths; tools.json regenerated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adron added a commit that referenced this pull request Oct 22, 2026
Brings in the Substratal-Version header and root paths (#37), the
plan-job secret fix (#59), and the versioning docs (#63).

- Conflicts in Applications (API and domain model), NFR, and
  plan-remaining.md: this branch's text, with root paths and anchors;
  main's side only differed by the removed /v1 prefix. The Changelog
  puts this branch's entry above main's two 2026-10-21 entries
- openapi.yaml 1.0.0-draft.5 (main took draft.4 for the header change)
- applications_test.go, the self_registered description, and an Auth
  anchor: root paths; tools.json regenerated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adron added a commit that referenced this pull request Oct 22, 2026
Brings in the Substratal-Version header and root paths (#37), the
plan-job secret fix (#59), and the versioning docs (#63).

- Conflicts in Audit, Orders & Audit, and NFR: this branch's rows, with
  root paths; main's side only differed by the removed /v1 prefix. The
  Changelog puts this branch's entry above main's two 2026-10-21 entries
- openapi.yaml 1.0.0-draft.5 (main took draft.4 for the header change)
- Root paths in audit_exports_test.go and in comments, descriptions,
  and docs this branch added; tools.json regenerated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adron added a commit that referenced this pull request Oct 22, 2026
Brings in the Substratal-Version header and root paths (#37), the
plan-job secret fix (#59), and the versioning docs (#63).

- /scim/v2 is exempt from Substratal-Version: identity providers (Okta,
  Entra ID, Google Workspace) can't add custom headers, and SCIM versions
  itself in the path. Without this every IdP request would be
  400 api_version_required. The scim-token endpoints still need it.
  Conventions, SCIM, openapi.yaml's description, and the Changelog say so
- scimCall sends no Substratal-Version, the way an IdP does, so the
  whole SCIM suite exercises the exemption; testenv skips contract
  validation for /scim/ (it follows RFC 7644, not openapi.yaml)
- Conflicts in Organizations and Orders & Audit: this branch's rows, with
  root paths; plan-remaining.md is main's (this branch didn't change it).
  The Changelog puts this branch's entry above main's two entries
- openapi.yaml 1.0.0-draft.5 (main took draft.4 for the header change)
- Root paths in scim.md, scim.go, and scim_test.go; tools.json regenerated

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant