> For the complete documentation index, see [llms.txt](https://docs.mymedtrics.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.mymedtrics.com/api-reference/connect-api/webhooks.md).

# Webhooks

Webhook subscriptions, event delivery history, and test delivery for API app integrations.

## List webhook subscriptions.

> Returns cursor-paged webhook subscriptions owned by the authenticated API app.

```json
{"openapi":"3.1.0","info":{"title":"Medtrics Connect API","version":"2026-10-27"},"tags":[{"name":"Webhooks","description":"Webhook subscriptions, event delivery history, and test delivery for API app integrations."}],"servers":[{"url":"https://sandbox.mymedtrics.com","description":"Client sandbox — shared demo tenant, X-Medtrics-Sandbox-Key or a sandbox API app"},{"url":"https://{client}.mymedtrics.com","description":"Your Medtrics instance","variables":{"client":{"default":"<client-name>","description":"The tenant subdomain Medtrics assigns to your institution"}}}],"security":[{"BearerToken":[]},{"SandboxApiKey":[]}],"components":{"securitySchemes":{"BearerToken":{"type":"http","scheme":"bearer"},"SandboxApiKey":{"type":"apiKey","in":"header","name":"X-Medtrics-Sandbox-Key"}},"parameters":{"ApiDateHeader":{"name":"Medtrics-API-Date","in":"header","required":true,"description":"API contract date. Use 2026-10-27 for this version. Contract dates select request behavior: rotation creates require explicit placement references from 2026-10-27; earlier accepted dates retain compatibility handling. Unknown dates return HTTP 400 with error.code unsupported_api_date.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"Limit":{"name":"limit","in":"query","required":false,"description":"Maximum number of records to return per page. The server may return fewer rows than requested. Values above the maximum (100) are silently clamped to 100. Values below the minimum (1) are silently clamped to 1. The effective value after clamping is always returned in pagination.limit so callers can detect that they hit the gateway ceiling. Per-client max_page_size overrides are documented as a tenant-configurable lower cap that never raises the public ceiling.\n","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},"Cursor":{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}},"responses":{"WebhookListResponse":{"description":"Cursor-paged webhook subscription records.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["data","pagination"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/WebhookSubscription"}},"pagination":{"$ref":"#/components/schemas/Pagination"}}}}}},"Unauthorized":{"description":"Missing or invalid credentials.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Forbidden":{"description":"Credential lacks tenant visibility or required scope.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"RateLimited":{"description":"Request or API app rate limit exceeded.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"InternalError":{"description":"Unexpected server error.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"headers":{"MedtricsRequestId":{"description":"Correlates all gateway logs for the request.","schema":{"type":"string"}},"MedtricsApiDate":{"description":"API contract date applied to the request.","schema":{"type":"string"}},"RateLimitLimit":{"description":"Request limit for the current window.","schema":{"type":"string"}},"RateLimitRemaining":{"description":"Remaining requests in the current window.","schema":{"type":"string"}},"RateLimitReset":{"description":"Unix seconds when the current rate-limit window resets.","schema":{"type":"string"}}},"schemas":{"WebhookSubscription":{"type":"object","description":"A webhook subscription owned by an API app. Never includes signing_secret.","required":["id","name","target_url","event_types","status","created_at"],"properties":{"id":{"type":"string","description":"Public ID of the webhook subscription (wh_ prefix)."},"name":{"type":"string"},"target_url":{"type":"string","format":"uri"},"event_types":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEventType"}},"status":{"$ref":"#/components/schemas/WebhookStatus"},"last_success_at":{"type":["string","null"],"format":"date-time"},"last_failure_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"WebhookEventType":{"type":"string","description":"Approved event type for webhook subscriptions.","enum":["program.updated","learner.updated","rotation.assignment_changed","evaluation.completed","import_job.completed"]},"WebhookStatus":{"type":"string","description":"Operational status of a webhook subscription. Canonical source is the webhook_status DB enum.","enum":["active","disabled","failing","suspended"]},"Pagination":{"type":"object","description":"Cursor pagination envelope. Exposes only limit and next_cursor. Count fields (approximate_total, total_count_exact, or any equivalent) are intentionally absent and reserved for future Medtrics-API-Date additions once a concrete partner use case justifies a count strategy per route. pagination.limit is the effective page size after server-side clamp, not the value the client requested.","required":["limit","next_cursor"],"properties":{"limit":{"type":"integer","description":"Effective page size after server-side clamping. Compare to your requested limit to detect ceiling enforcement."},"next_cursor":{"type":["string","null"],"description":"Opaque keyset cursor. Pass as the cursor query parameter on the next request. null when no more pages exist."}}},"ErrorEnvelope":{"type":"object","required":["error"],"description":"Structured error envelope. error.code is the machine-readable discriminant. Typed detail schemas are provided for codes where structured data aids client remediation. Use the discriminator.mapping to select the correct variant schema; for all other codes see GenericApiError.\n","properties":{"error":{"anyOf":[{"$ref":"#/components/schemas/InvalidStatusError"},{"$ref":"#/components/schemas/InvalidWebhookEventsError"},{"$ref":"#/components/schemas/RateLimitedError"},{"$ref":"#/components/schemas/CursorResourceMismatchError"},{"$ref":"#/components/schemas/InvalidIdError"},{"$ref":"#/components/schemas/UnsupportedApiDateError"},{"$ref":"#/components/schemas/InactiveClientError"},{"$ref":"#/components/schemas/InvalidWebhookUrlError"},{"$ref":"#/components/schemas/GenericApiError"}],"discriminator":{"propertyName":"code","mapping":{"invalid_status":"#/components/schemas/InvalidStatusError","invalid_webhook_events":"#/components/schemas/InvalidWebhookEventsError","rate_limited":"#/components/schemas/RateLimitedError","cursor_resource_mismatch":"#/components/schemas/CursorResourceMismatchError","invalid_id":"#/components/schemas/InvalidIdError","unsupported_api_date":"#/components/schemas/UnsupportedApiDateError","inactive_client":"#/components/schemas/InactiveClientError","invalid_webhook_url":"#/components/schemas/InvalidWebhookUrlError"}}}}},"InvalidStatusError":{"description":"Returned when a status query parameter value is outside the allowed enum. HTTP 400. details.field identifies the parameter; details.allowed lists every accepted value.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_status"},"details":{"type":"object","required":["field","allowed"],"additionalProperties":false,"description":"Structured remediation data for invalid_status.","properties":{"field":{"type":"string","description":"Query parameter name that received the invalid value (e.g. enrollment_status, status).\n"},"allowed":{"type":"array","items":{"type":"string"},"description":"Full list of accepted values for the parameter."}}}}}]},"ApiErrorBase":{"type":"object","required":["code","message","request_id"],"description":"Common fields shared by every Connect API error response.","properties":{"code":{"type":"string","description":"Machine-readable error code."},"message":{"type":"string","description":"Human-readable error description."},"request_id":{"type":"string","description":"Request correlation ID. Include this value when reporting issues to support alongside the Medtrics-Request-ID response header.\n"}}},"InvalidWebhookEventsError":{"description":"Returned when event_types contains values not in the approved Connect API event list, or when event_types is empty. HTTP 400. details.unsupported_events lists the rejected entries (empty array when event_types was missing or entirely empty).\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_webhook_events"},"details":{"type":"object","required":["unsupported_events"],"additionalProperties":false,"description":"Structured remediation data for invalid_webhook_events.","properties":{"unsupported_events":{"type":"array","items":{"type":"string"},"description":"Event type strings that are not approved Connect API event names. Empty array indicates event_types was missing or contained only invalid entries.\n"}}}}}]},"RateLimitedError":{"description":"Returned when the request or API app rate limit is exceeded. HTTP 429. details.retry_after_seconds indicates the wait duration before retrying.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"rate_limited"},"details":{"type":"object","required":["retry_after_seconds"],"additionalProperties":false,"description":"Structured remediation data for rate_limited.","properties":{"retry_after_seconds":{"type":"integer","description":"Seconds to wait before retrying. Mirrors the Retry-After response header value.\n"}}}}}]},"CursorResourceMismatchError":{"description":"Returned when the cursor was issued for a different resource type than the endpoint being called. HTTP 400. details.expected_resource is the resource this endpoint serves; details.cursor_resource is what the cursor encodes.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"cursor_resource_mismatch"},"details":{"type":"object","required":["expected_resource","cursor_resource"],"additionalProperties":false,"description":"Structured remediation data for cursor_resource_mismatch.","properties":{"expected_resource":{"type":"string","description":"Resource type this endpoint serves (e.g. programs, learners)."},"cursor_resource":{"type":"string","description":"Resource type encoded in the submitted cursor."}}}}}]},"InvalidIdError":{"description":"Returned when a path parameter public ID does not match the expected prefix for the resource. HTTP 400. details.field is the parameter name; details.expected_prefix is the required ID prefix.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_id"},"details":{"type":"object","required":["field","expected_prefix"],"additionalProperties":false,"description":"Structured remediation data for invalid_id.","properties":{"field":{"type":"string","description":"Path or query parameter name that received the invalid ID."},"expected_prefix":{"type":"string","description":"Required ID prefix for this parameter."}}}}}]},"UnsupportedApiDateError":{"description":"Returned when the Medtrics-API-Date header is missing, malformed (not YYYY-MM-DD), or not a supported contract date. HTTP 400. details.supported_dates lists all accepted values. details.reason is one of \"missing\", \"malformed\", or \"not_supported\".\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"unsupported_api_date"},"details":{"type":"object","required":["reason","supported_dates"],"additionalProperties":false,"description":"Structured remediation data for unsupported_api_date.","properties":{"reason":{"type":"string","enum":["missing","malformed","not_supported"],"description":"Specific cause: \"missing\" when the header is absent, \"malformed\" when the value is not YYYY-MM-DD format, \"not_supported\" when the format is valid but the date is not a known contract date.\n"},"supported_dates":{"type":"array","items":{"type":"string"},"description":"All accepted Medtrics-API-Date values."}}}}}]},"InactiveClientError":{"description":"Returned when the authenticated API client is revoked, expired, deleted, or in a non-active status. HTTP 403. No structured details beyond the human-readable message.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"code":{"type":"string","const":"inactive_client"}}}]},"InvalidWebhookUrlError":{"description":"Returned when target_url fails HTTPS validation or is otherwise rejected. HTTP 422. No structured details field beyond the human-readable message.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"code":{"type":"string","const":"invalid_webhook_url"}}}]},"GenericApiError":{"description":"Catch-all error variant for codes that carry no structured remediation data (invalid_cursor, not_found, invalid_credentials, scope_not_allowed, invalid_webhook_tolerance, internal_error, and any future codes not covered by a typed variant in the discriminator.mapping). details is absent or empty for these codes.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"details":{"type":"object","additionalProperties":true,"description":"Absent or empty for most generic error codes."}}}]}}},"paths":{"/connect/webhooks":{"get":{"operationId":"listWebhookSubscriptions","tags":["Webhooks"],"summary":"List webhook subscriptions.","description":"Returns cursor-paged webhook subscriptions owned by the authenticated API app.","parameters":[{"$ref":"#/components/parameters/ApiDateHeader"},{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"}],"responses":{"200":{"$ref":"#/components/responses/WebhookListResponse"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}}}}
```

## Create a webhook subscription.

> Creates an active webhook subscription with HTTPS URL validation. On first call returns 201 with signing\_secret (whsec\_ prefix) in the response body — store it immediately, it cannot be recovered later. No subsequent request, including an idempotency-key replay, can reveal it again. Idempotency-key replay (same Idempotency-Key header) returns HTTP 200 with the same webhook.id but without signing\_secret. HTTP 201 vs 200 distinguishes a fresh creation from a deduplicated replay. target\_url must be HTTPS. event\_types must contain at least one approved value (see WebhookEventType).<br>

```json
{"openapi":"3.1.0","info":{"title":"Medtrics Connect API","version":"2026-10-27"},"tags":[{"name":"Webhooks","description":"Webhook subscriptions, event delivery history, and test delivery for API app integrations."}],"servers":[{"url":"https://sandbox.mymedtrics.com","description":"Client sandbox — shared demo tenant, X-Medtrics-Sandbox-Key or a sandbox API app"},{"url":"https://{client}.mymedtrics.com","description":"Your Medtrics instance","variables":{"client":{"default":"<client-name>","description":"The tenant subdomain Medtrics assigns to your institution"}}}],"security":[{"BearerToken":[]},{"SandboxApiKey":[]}],"components":{"securitySchemes":{"BearerToken":{"type":"http","scheme":"bearer"},"SandboxApiKey":{"type":"apiKey","in":"header","name":"X-Medtrics-Sandbox-Key"}},"parameters":{"ApiDateHeader":{"name":"Medtrics-API-Date","in":"header","required":true,"description":"API contract date. Use 2026-10-27 for this version. Contract dates select request behavior: rotation creates require explicit placement references from 2026-10-27; earlier accepted dates retain compatibility handling. Unknown dates return HTTP 400 with error.code unsupported_api_date.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"description":"Optional deduplication key for create operations. When the same key is supplied on a repeated request, the server returns the existing resource instead of creating a new one. For POST /connect/webhooks: replay returns HTTP 200 with the existing webhook but without signing_secret. The Idempotency-Key is a deduplication token, not a secret recovery mechanism.\n","schema":{"type":"string","maxLength":255}}},"schemas":{"CreateWebhookSubscriptionRequest":{"type":"object","required":["name","target_url","event_types"],"properties":{"name":{"type":"string"},"target_url":{"type":"string","format":"uri"},"event_types":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEventType"}},"timestamp_tolerance_seconds":{"type":"integer","minimum":60,"maximum":600}}},"WebhookEventType":{"type":"string","description":"Approved event type for webhook subscriptions.","enum":["program.updated","learner.updated","rotation.assignment_changed","evaluation.completed","import_job.completed"]},"WebhookSubscription":{"type":"object","description":"A webhook subscription owned by an API app. Never includes signing_secret.","required":["id","name","target_url","event_types","status","created_at"],"properties":{"id":{"type":"string","description":"Public ID of the webhook subscription (wh_ prefix)."},"name":{"type":"string"},"target_url":{"type":"string","format":"uri"},"event_types":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEventType"}},"status":{"$ref":"#/components/schemas/WebhookStatus"},"last_success_at":{"type":["string","null"],"format":"date-time"},"last_failure_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"WebhookStatus":{"type":"string","description":"Operational status of a webhook subscription. Canonical source is the webhook_status DB enum.","enum":["active","disabled","failing","suspended"]},"WebhookSubscriptionCreateResponse":{"allOf":[{"$ref":"#/components/schemas/WebhookSubscription"},{"type":"object","description":"Returned only in the 201 body for POST /connect/webhooks (first call per Idempotency-Key). signing_secret (whsec_ prefix) is present exactly once and cannot be recovered later. Idempotency-key replay returns HTTP 200 with WebhookSubscription (no signing_secret).\n","required":["signing_secret"],"properties":{"signing_secret":{"type":"string","description":"Webhook signing secret (whsec_ prefix). Present only in the 201 response. Store it immediately — no subsequent request, including idempotency-key replay, can reveal it again.\n"}}}]},"ErrorEnvelope":{"type":"object","required":["error"],"description":"Structured error envelope. error.code is the machine-readable discriminant. Typed detail schemas are provided for codes where structured data aids client remediation. Use the discriminator.mapping to select the correct variant schema; for all other codes see GenericApiError.\n","properties":{"error":{"anyOf":[{"$ref":"#/components/schemas/InvalidStatusError"},{"$ref":"#/components/schemas/InvalidWebhookEventsError"},{"$ref":"#/components/schemas/RateLimitedError"},{"$ref":"#/components/schemas/CursorResourceMismatchError"},{"$ref":"#/components/schemas/InvalidIdError"},{"$ref":"#/components/schemas/UnsupportedApiDateError"},{"$ref":"#/components/schemas/InactiveClientError"},{"$ref":"#/components/schemas/InvalidWebhookUrlError"},{"$ref":"#/components/schemas/GenericApiError"}],"discriminator":{"propertyName":"code","mapping":{"invalid_status":"#/components/schemas/InvalidStatusError","invalid_webhook_events":"#/components/schemas/InvalidWebhookEventsError","rate_limited":"#/components/schemas/RateLimitedError","cursor_resource_mismatch":"#/components/schemas/CursorResourceMismatchError","invalid_id":"#/components/schemas/InvalidIdError","unsupported_api_date":"#/components/schemas/UnsupportedApiDateError","inactive_client":"#/components/schemas/InactiveClientError","invalid_webhook_url":"#/components/schemas/InvalidWebhookUrlError"}}}}},"InvalidStatusError":{"description":"Returned when a status query parameter value is outside the allowed enum. HTTP 400. details.field identifies the parameter; details.allowed lists every accepted value.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_status"},"details":{"type":"object","required":["field","allowed"],"additionalProperties":false,"description":"Structured remediation data for invalid_status.","properties":{"field":{"type":"string","description":"Query parameter name that received the invalid value (e.g. enrollment_status, status).\n"},"allowed":{"type":"array","items":{"type":"string"},"description":"Full list of accepted values for the parameter."}}}}}]},"ApiErrorBase":{"type":"object","required":["code","message","request_id"],"description":"Common fields shared by every Connect API error response.","properties":{"code":{"type":"string","description":"Machine-readable error code."},"message":{"type":"string","description":"Human-readable error description."},"request_id":{"type":"string","description":"Request correlation ID. Include this value when reporting issues to support alongside the Medtrics-Request-ID response header.\n"}}},"InvalidWebhookEventsError":{"description":"Returned when event_types contains values not in the approved Connect API event list, or when event_types is empty. HTTP 400. details.unsupported_events lists the rejected entries (empty array when event_types was missing or entirely empty).\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_webhook_events"},"details":{"type":"object","required":["unsupported_events"],"additionalProperties":false,"description":"Structured remediation data for invalid_webhook_events.","properties":{"unsupported_events":{"type":"array","items":{"type":"string"},"description":"Event type strings that are not approved Connect API event names. Empty array indicates event_types was missing or contained only invalid entries.\n"}}}}}]},"RateLimitedError":{"description":"Returned when the request or API app rate limit is exceeded. HTTP 429. details.retry_after_seconds indicates the wait duration before retrying.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"rate_limited"},"details":{"type":"object","required":["retry_after_seconds"],"additionalProperties":false,"description":"Structured remediation data for rate_limited.","properties":{"retry_after_seconds":{"type":"integer","description":"Seconds to wait before retrying. Mirrors the Retry-After response header value.\n"}}}}}]},"CursorResourceMismatchError":{"description":"Returned when the cursor was issued for a different resource type than the endpoint being called. HTTP 400. details.expected_resource is the resource this endpoint serves; details.cursor_resource is what the cursor encodes.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"cursor_resource_mismatch"},"details":{"type":"object","required":["expected_resource","cursor_resource"],"additionalProperties":false,"description":"Structured remediation data for cursor_resource_mismatch.","properties":{"expected_resource":{"type":"string","description":"Resource type this endpoint serves (e.g. programs, learners)."},"cursor_resource":{"type":"string","description":"Resource type encoded in the submitted cursor."}}}}}]},"InvalidIdError":{"description":"Returned when a path parameter public ID does not match the expected prefix for the resource. HTTP 400. details.field is the parameter name; details.expected_prefix is the required ID prefix.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_id"},"details":{"type":"object","required":["field","expected_prefix"],"additionalProperties":false,"description":"Structured remediation data for invalid_id.","properties":{"field":{"type":"string","description":"Path or query parameter name that received the invalid ID."},"expected_prefix":{"type":"string","description":"Required ID prefix for this parameter."}}}}}]},"UnsupportedApiDateError":{"description":"Returned when the Medtrics-API-Date header is missing, malformed (not YYYY-MM-DD), or not a supported contract date. HTTP 400. details.supported_dates lists all accepted values. details.reason is one of \"missing\", \"malformed\", or \"not_supported\".\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"unsupported_api_date"},"details":{"type":"object","required":["reason","supported_dates"],"additionalProperties":false,"description":"Structured remediation data for unsupported_api_date.","properties":{"reason":{"type":"string","enum":["missing","malformed","not_supported"],"description":"Specific cause: \"missing\" when the header is absent, \"malformed\" when the value is not YYYY-MM-DD format, \"not_supported\" when the format is valid but the date is not a known contract date.\n"},"supported_dates":{"type":"array","items":{"type":"string"},"description":"All accepted Medtrics-API-Date values."}}}}}]},"InactiveClientError":{"description":"Returned when the authenticated API client is revoked, expired, deleted, or in a non-active status. HTTP 403. No structured details beyond the human-readable message.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"code":{"type":"string","const":"inactive_client"}}}]},"InvalidWebhookUrlError":{"description":"Returned when target_url fails HTTPS validation or is otherwise rejected. HTTP 422. No structured details field beyond the human-readable message.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"code":{"type":"string","const":"invalid_webhook_url"}}}]},"GenericApiError":{"description":"Catch-all error variant for codes that carry no structured remediation data (invalid_cursor, not_found, invalid_credentials, scope_not_allowed, invalid_webhook_tolerance, internal_error, and any future codes not covered by a typed variant in the discriminator.mapping). details is absent or empty for these codes.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"details":{"type":"object","additionalProperties":true,"description":"Absent or empty for most generic error codes."}}}]}},"headers":{"MedtricsRequestId":{"description":"Correlates all gateway logs for the request.","schema":{"type":"string"}},"MedtricsApiDate":{"description":"API contract date applied to the request.","schema":{"type":"string"}},"RateLimitLimit":{"description":"Request limit for the current window.","schema":{"type":"string"}},"RateLimitRemaining":{"description":"Remaining requests in the current window.","schema":{"type":"string"}},"RateLimitReset":{"description":"Unix seconds when the current rate-limit window resets.","schema":{"type":"string"}}},"responses":{"BadRequest":{"description":"Invalid request parameters.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Unauthorized":{"description":"Missing or invalid credentials.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Forbidden":{"description":"Credential lacks tenant visibility or required scope.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Conflict":{"description":"A resource with a conflicting unique field already exists (409). Code: duplicate_resource. details.field names the request field to change, for example email or employee_number on a learner, program_id when the learner is already actively enrolled in that program, or name on a webhook subscription. Key values are never echoed.\n","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"UnprocessableEntity":{"description":"Request payload is syntactically valid but fails HTTPS validation. Used for createWebhookSubscription when target_url is not a valid HTTPS URL. invalid_webhook_events (HTTP 400) and invalid_webhook_tolerance (HTTP 400) are returned via the BadRequest response, not here.\n","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"RateLimited":{"description":"Request or API app rate limit exceeded.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"InternalError":{"description":"Unexpected server error.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"paths":{"/connect/webhooks":{"post":{"operationId":"createWebhookSubscription","tags":["Webhooks"],"summary":"Create a webhook subscription.","description":"Creates an active webhook subscription with HTTPS URL validation. On first call returns 201 with signing_secret (whsec_ prefix) in the response body — store it immediately, it cannot be recovered later. No subsequent request, including an idempotency-key replay, can reveal it again. Idempotency-key replay (same Idempotency-Key header) returns HTTP 200 with the same webhook.id but without signing_secret. HTTP 201 vs 200 distinguishes a fresh creation from a deduplicated replay. target_url must be HTTPS. event_types must contain at least one approved value (see WebhookEventType).\n","parameters":[{"$ref":"#/components/parameters/ApiDateHeader"},{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateWebhookSubscriptionRequest"}}}},"responses":{"200":{"description":"Idempotency-key replay. Webhook already exists; signing_secret is absent.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookSubscription"}}}}}},"201":{"description":"Webhook subscription created. signing_secret is present only in this 201 response — store it immediately.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookSubscriptionCreateResponse"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"$ref":"#/components/responses/Conflict"},"422":{"$ref":"#/components/responses/UnprocessableEntity"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}}}}
```

## Get a webhook subscription.

> Returns one webhook subscription owned by the authenticated API app. Never includes signing\_secret.

```json
{"openapi":"3.1.0","info":{"title":"Medtrics Connect API","version":"2026-10-27"},"tags":[{"name":"Webhooks","description":"Webhook subscriptions, event delivery history, and test delivery for API app integrations."}],"servers":[{"url":"https://sandbox.mymedtrics.com","description":"Client sandbox — shared demo tenant, X-Medtrics-Sandbox-Key or a sandbox API app"},{"url":"https://{client}.mymedtrics.com","description":"Your Medtrics instance","variables":{"client":{"default":"<client-name>","description":"The tenant subdomain Medtrics assigns to your institution"}}}],"security":[{"BearerToken":[]},{"SandboxApiKey":[]}],"components":{"securitySchemes":{"BearerToken":{"type":"http","scheme":"bearer"},"SandboxApiKey":{"type":"apiKey","in":"header","name":"X-Medtrics-Sandbox-Key"}},"parameters":{"ApiDateHeader":{"name":"Medtrics-API-Date","in":"header","required":true,"description":"API contract date. Use 2026-10-27 for this version. Contract dates select request behavior: rotation creates require explicit placement references from 2026-10-27; earlier accepted dates retain compatibility handling. Unknown dates return HTTP 400 with error.code unsupported_api_date.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"WebhookId":{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"},"description":"Public ID of the webhook subscription (wh_ prefix). Backed by api_webhook_subscriptions.id."}},"responses":{"WebhookItemResponse":{"description":"Single webhook subscription record. Never includes signing_secret.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookSubscription"}}}}}},"Unauthorized":{"description":"Missing or invalid credentials.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Forbidden":{"description":"Credential lacks tenant visibility or required scope.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"RateLimited":{"description":"Request or API app rate limit exceeded.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"InternalError":{"description":"Unexpected server error.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"headers":{"MedtricsRequestId":{"description":"Correlates all gateway logs for the request.","schema":{"type":"string"}},"MedtricsApiDate":{"description":"API contract date applied to the request.","schema":{"type":"string"}},"RateLimitLimit":{"description":"Request limit for the current window.","schema":{"type":"string"}},"RateLimitRemaining":{"description":"Remaining requests in the current window.","schema":{"type":"string"}},"RateLimitReset":{"description":"Unix seconds when the current rate-limit window resets.","schema":{"type":"string"}}},"schemas":{"WebhookSubscription":{"type":"object","description":"A webhook subscription owned by an API app. Never includes signing_secret.","required":["id","name","target_url","event_types","status","created_at"],"properties":{"id":{"type":"string","description":"Public ID of the webhook subscription (wh_ prefix)."},"name":{"type":"string"},"target_url":{"type":"string","format":"uri"},"event_types":{"type":"array","items":{"$ref":"#/components/schemas/WebhookEventType"}},"status":{"$ref":"#/components/schemas/WebhookStatus"},"last_success_at":{"type":["string","null"],"format":"date-time"},"last_failure_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"WebhookEventType":{"type":"string","description":"Approved event type for webhook subscriptions.","enum":["program.updated","learner.updated","rotation.assignment_changed","evaluation.completed","import_job.completed"]},"WebhookStatus":{"type":"string","description":"Operational status of a webhook subscription. Canonical source is the webhook_status DB enum.","enum":["active","disabled","failing","suspended"]},"ErrorEnvelope":{"type":"object","required":["error"],"description":"Structured error envelope. error.code is the machine-readable discriminant. Typed detail schemas are provided for codes where structured data aids client remediation. Use the discriminator.mapping to select the correct variant schema; for all other codes see GenericApiError.\n","properties":{"error":{"anyOf":[{"$ref":"#/components/schemas/InvalidStatusError"},{"$ref":"#/components/schemas/InvalidWebhookEventsError"},{"$ref":"#/components/schemas/RateLimitedError"},{"$ref":"#/components/schemas/CursorResourceMismatchError"},{"$ref":"#/components/schemas/InvalidIdError"},{"$ref":"#/components/schemas/UnsupportedApiDateError"},{"$ref":"#/components/schemas/InactiveClientError"},{"$ref":"#/components/schemas/InvalidWebhookUrlError"},{"$ref":"#/components/schemas/GenericApiError"}],"discriminator":{"propertyName":"code","mapping":{"invalid_status":"#/components/schemas/InvalidStatusError","invalid_webhook_events":"#/components/schemas/InvalidWebhookEventsError","rate_limited":"#/components/schemas/RateLimitedError","cursor_resource_mismatch":"#/components/schemas/CursorResourceMismatchError","invalid_id":"#/components/schemas/InvalidIdError","unsupported_api_date":"#/components/schemas/UnsupportedApiDateError","inactive_client":"#/components/schemas/InactiveClientError","invalid_webhook_url":"#/components/schemas/InvalidWebhookUrlError"}}}}},"InvalidStatusError":{"description":"Returned when a status query parameter value is outside the allowed enum. HTTP 400. details.field identifies the parameter; details.allowed lists every accepted value.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_status"},"details":{"type":"object","required":["field","allowed"],"additionalProperties":false,"description":"Structured remediation data for invalid_status.","properties":{"field":{"type":"string","description":"Query parameter name that received the invalid value (e.g. enrollment_status, status).\n"},"allowed":{"type":"array","items":{"type":"string"},"description":"Full list of accepted values for the parameter."}}}}}]},"ApiErrorBase":{"type":"object","required":["code","message","request_id"],"description":"Common fields shared by every Connect API error response.","properties":{"code":{"type":"string","description":"Machine-readable error code."},"message":{"type":"string","description":"Human-readable error description."},"request_id":{"type":"string","description":"Request correlation ID. Include this value when reporting issues to support alongside the Medtrics-Request-ID response header.\n"}}},"InvalidWebhookEventsError":{"description":"Returned when event_types contains values not in the approved Connect API event list, or when event_types is empty. HTTP 400. details.unsupported_events lists the rejected entries (empty array when event_types was missing or entirely empty).\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_webhook_events"},"details":{"type":"object","required":["unsupported_events"],"additionalProperties":false,"description":"Structured remediation data for invalid_webhook_events.","properties":{"unsupported_events":{"type":"array","items":{"type":"string"},"description":"Event type strings that are not approved Connect API event names. Empty array indicates event_types was missing or contained only invalid entries.\n"}}}}}]},"RateLimitedError":{"description":"Returned when the request or API app rate limit is exceeded. HTTP 429. details.retry_after_seconds indicates the wait duration before retrying.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"rate_limited"},"details":{"type":"object","required":["retry_after_seconds"],"additionalProperties":false,"description":"Structured remediation data for rate_limited.","properties":{"retry_after_seconds":{"type":"integer","description":"Seconds to wait before retrying. Mirrors the Retry-After response header value.\n"}}}}}]},"CursorResourceMismatchError":{"description":"Returned when the cursor was issued for a different resource type than the endpoint being called. HTTP 400. details.expected_resource is the resource this endpoint serves; details.cursor_resource is what the cursor encodes.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"cursor_resource_mismatch"},"details":{"type":"object","required":["expected_resource","cursor_resource"],"additionalProperties":false,"description":"Structured remediation data for cursor_resource_mismatch.","properties":{"expected_resource":{"type":"string","description":"Resource type this endpoint serves (e.g. programs, learners)."},"cursor_resource":{"type":"string","description":"Resource type encoded in the submitted cursor."}}}}}]},"InvalidIdError":{"description":"Returned when a path parameter public ID does not match the expected prefix for the resource. HTTP 400. details.field is the parameter name; details.expected_prefix is the required ID prefix.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_id"},"details":{"type":"object","required":["field","expected_prefix"],"additionalProperties":false,"description":"Structured remediation data for invalid_id.","properties":{"field":{"type":"string","description":"Path or query parameter name that received the invalid ID."},"expected_prefix":{"type":"string","description":"Required ID prefix for this parameter."}}}}}]},"UnsupportedApiDateError":{"description":"Returned when the Medtrics-API-Date header is missing, malformed (not YYYY-MM-DD), or not a supported contract date. HTTP 400. details.supported_dates lists all accepted values. details.reason is one of \"missing\", \"malformed\", or \"not_supported\".\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"unsupported_api_date"},"details":{"type":"object","required":["reason","supported_dates"],"additionalProperties":false,"description":"Structured remediation data for unsupported_api_date.","properties":{"reason":{"type":"string","enum":["missing","malformed","not_supported"],"description":"Specific cause: \"missing\" when the header is absent, \"malformed\" when the value is not YYYY-MM-DD format, \"not_supported\" when the format is valid but the date is not a known contract date.\n"},"supported_dates":{"type":"array","items":{"type":"string"},"description":"All accepted Medtrics-API-Date values."}}}}}]},"InactiveClientError":{"description":"Returned when the authenticated API client is revoked, expired, deleted, or in a non-active status. HTTP 403. No structured details beyond the human-readable message.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"code":{"type":"string","const":"inactive_client"}}}]},"InvalidWebhookUrlError":{"description":"Returned when target_url fails HTTPS validation or is otherwise rejected. HTTP 422. No structured details field beyond the human-readable message.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"code":{"type":"string","const":"invalid_webhook_url"}}}]},"GenericApiError":{"description":"Catch-all error variant for codes that carry no structured remediation data (invalid_cursor, not_found, invalid_credentials, scope_not_allowed, invalid_webhook_tolerance, internal_error, and any future codes not covered by a typed variant in the discriminator.mapping). details is absent or empty for these codes.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"details":{"type":"object","additionalProperties":true,"description":"Absent or empty for most generic error codes."}}}]}}},"paths":{"/connect/webhooks/{webhookId}":{"get":{"operationId":"getWebhookSubscription","tags":["Webhooks"],"summary":"Get a webhook subscription.","description":"Returns one webhook subscription owned by the authenticated API app. Never includes signing_secret.","parameters":[{"$ref":"#/components/parameters/ApiDateHeader"},{"$ref":"#/components/parameters/WebhookId"}],"responses":{"200":{"$ref":"#/components/responses/WebhookItemResponse"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}}}}
```

## Delete a webhook subscription.

> Soft-deletes a webhook subscription owned by the authenticated API app. Requires webhooks:write scope. The subscription stops receiving events, no longer counts toward the app's subscription limit, and its name can be reused. Deliveries still queued for it are cancelled. A subscription that does not exist, is already deleted, or belongs to another app returns 404.

```json
{"openapi":"3.1.0","info":{"title":"Medtrics Connect API","version":"2026-10-27"},"tags":[{"name":"Webhooks","description":"Webhook subscriptions, event delivery history, and test delivery for API app integrations."}],"servers":[{"url":"https://sandbox.mymedtrics.com","description":"Client sandbox — shared demo tenant, X-Medtrics-Sandbox-Key or a sandbox API app"},{"url":"https://{client}.mymedtrics.com","description":"Your Medtrics instance","variables":{"client":{"default":"<client-name>","description":"The tenant subdomain Medtrics assigns to your institution"}}}],"security":[{"BearerToken":[]},{"SandboxApiKey":[]}],"components":{"securitySchemes":{"BearerToken":{"type":"http","scheme":"bearer"},"SandboxApiKey":{"type":"apiKey","in":"header","name":"X-Medtrics-Sandbox-Key"}},"parameters":{"ApiDateHeader":{"name":"Medtrics-API-Date","in":"header","required":true,"description":"API contract date. Use 2026-10-27 for this version. Contract dates select request behavior: rotation creates require explicit placement references from 2026-10-27; earlier accepted dates retain compatibility handling. Unknown dates return HTTP 400 with error.code unsupported_api_date.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"WebhookId":{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"},"description":"Public ID of the webhook subscription (wh_ prefix). Backed by api_webhook_subscriptions.id."}},"responses":{"NoContent":{"description":"Success with no response body.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}}},"BadRequest":{"description":"Invalid request parameters.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Unauthorized":{"description":"Missing or invalid credentials.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Forbidden":{"description":"Credential lacks tenant visibility or required scope.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"NotFound":{"description":"Resource not found or outside tenant scope.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"RateLimited":{"description":"Request or API app rate limit exceeded.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"InternalError":{"description":"Unexpected server error.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}},"headers":{"MedtricsRequestId":{"description":"Correlates all gateway logs for the request.","schema":{"type":"string"}},"MedtricsApiDate":{"description":"API contract date applied to the request.","schema":{"type":"string"}},"RateLimitLimit":{"description":"Request limit for the current window.","schema":{"type":"string"}},"RateLimitRemaining":{"description":"Remaining requests in the current window.","schema":{"type":"string"}},"RateLimitReset":{"description":"Unix seconds when the current rate-limit window resets.","schema":{"type":"string"}}},"schemas":{"ErrorEnvelope":{"type":"object","required":["error"],"description":"Structured error envelope. error.code is the machine-readable discriminant. Typed detail schemas are provided for codes where structured data aids client remediation. Use the discriminator.mapping to select the correct variant schema; for all other codes see GenericApiError.\n","properties":{"error":{"anyOf":[{"$ref":"#/components/schemas/InvalidStatusError"},{"$ref":"#/components/schemas/InvalidWebhookEventsError"},{"$ref":"#/components/schemas/RateLimitedError"},{"$ref":"#/components/schemas/CursorResourceMismatchError"},{"$ref":"#/components/schemas/InvalidIdError"},{"$ref":"#/components/schemas/UnsupportedApiDateError"},{"$ref":"#/components/schemas/InactiveClientError"},{"$ref":"#/components/schemas/InvalidWebhookUrlError"},{"$ref":"#/components/schemas/GenericApiError"}],"discriminator":{"propertyName":"code","mapping":{"invalid_status":"#/components/schemas/InvalidStatusError","invalid_webhook_events":"#/components/schemas/InvalidWebhookEventsError","rate_limited":"#/components/schemas/RateLimitedError","cursor_resource_mismatch":"#/components/schemas/CursorResourceMismatchError","invalid_id":"#/components/schemas/InvalidIdError","unsupported_api_date":"#/components/schemas/UnsupportedApiDateError","inactive_client":"#/components/schemas/InactiveClientError","invalid_webhook_url":"#/components/schemas/InvalidWebhookUrlError"}}}}},"InvalidStatusError":{"description":"Returned when a status query parameter value is outside the allowed enum. HTTP 400. details.field identifies the parameter; details.allowed lists every accepted value.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_status"},"details":{"type":"object","required":["field","allowed"],"additionalProperties":false,"description":"Structured remediation data for invalid_status.","properties":{"field":{"type":"string","description":"Query parameter name that received the invalid value (e.g. enrollment_status, status).\n"},"allowed":{"type":"array","items":{"type":"string"},"description":"Full list of accepted values for the parameter."}}}}}]},"ApiErrorBase":{"type":"object","required":["code","message","request_id"],"description":"Common fields shared by every Connect API error response.","properties":{"code":{"type":"string","description":"Machine-readable error code."},"message":{"type":"string","description":"Human-readable error description."},"request_id":{"type":"string","description":"Request correlation ID. Include this value when reporting issues to support alongside the Medtrics-Request-ID response header.\n"}}},"InvalidWebhookEventsError":{"description":"Returned when event_types contains values not in the approved Connect API event list, or when event_types is empty. HTTP 400. details.unsupported_events lists the rejected entries (empty array when event_types was missing or entirely empty).\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_webhook_events"},"details":{"type":"object","required":["unsupported_events"],"additionalProperties":false,"description":"Structured remediation data for invalid_webhook_events.","properties":{"unsupported_events":{"type":"array","items":{"type":"string"},"description":"Event type strings that are not approved Connect API event names. Empty array indicates event_types was missing or contained only invalid entries.\n"}}}}}]},"RateLimitedError":{"description":"Returned when the request or API app rate limit is exceeded. HTTP 429. details.retry_after_seconds indicates the wait duration before retrying.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"rate_limited"},"details":{"type":"object","required":["retry_after_seconds"],"additionalProperties":false,"description":"Structured remediation data for rate_limited.","properties":{"retry_after_seconds":{"type":"integer","description":"Seconds to wait before retrying. Mirrors the Retry-After response header value.\n"}}}}}]},"CursorResourceMismatchError":{"description":"Returned when the cursor was issued for a different resource type than the endpoint being called. HTTP 400. details.expected_resource is the resource this endpoint serves; details.cursor_resource is what the cursor encodes.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"cursor_resource_mismatch"},"details":{"type":"object","required":["expected_resource","cursor_resource"],"additionalProperties":false,"description":"Structured remediation data for cursor_resource_mismatch.","properties":{"expected_resource":{"type":"string","description":"Resource type this endpoint serves (e.g. programs, learners)."},"cursor_resource":{"type":"string","description":"Resource type encoded in the submitted cursor."}}}}}]},"InvalidIdError":{"description":"Returned when a path parameter public ID does not match the expected prefix for the resource. HTTP 400. details.field is the parameter name; details.expected_prefix is the required ID prefix.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_id"},"details":{"type":"object","required":["field","expected_prefix"],"additionalProperties":false,"description":"Structured remediation data for invalid_id.","properties":{"field":{"type":"string","description":"Path or query parameter name that received the invalid ID."},"expected_prefix":{"type":"string","description":"Required ID prefix for this parameter."}}}}}]},"UnsupportedApiDateError":{"description":"Returned when the Medtrics-API-Date header is missing, malformed (not YYYY-MM-DD), or not a supported contract date. HTTP 400. details.supported_dates lists all accepted values. details.reason is one of \"missing\", \"malformed\", or \"not_supported\".\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"unsupported_api_date"},"details":{"type":"object","required":["reason","supported_dates"],"additionalProperties":false,"description":"Structured remediation data for unsupported_api_date.","properties":{"reason":{"type":"string","enum":["missing","malformed","not_supported"],"description":"Specific cause: \"missing\" when the header is absent, \"malformed\" when the value is not YYYY-MM-DD format, \"not_supported\" when the format is valid but the date is not a known contract date.\n"},"supported_dates":{"type":"array","items":{"type":"string"},"description":"All accepted Medtrics-API-Date values."}}}}}]},"InactiveClientError":{"description":"Returned when the authenticated API client is revoked, expired, deleted, or in a non-active status. HTTP 403. No structured details beyond the human-readable message.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"code":{"type":"string","const":"inactive_client"}}}]},"InvalidWebhookUrlError":{"description":"Returned when target_url fails HTTPS validation or is otherwise rejected. HTTP 422. No structured details field beyond the human-readable message.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"code":{"type":"string","const":"invalid_webhook_url"}}}]},"GenericApiError":{"description":"Catch-all error variant for codes that carry no structured remediation data (invalid_cursor, not_found, invalid_credentials, scope_not_allowed, invalid_webhook_tolerance, internal_error, and any future codes not covered by a typed variant in the discriminator.mapping). details is absent or empty for these codes.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"details":{"type":"object","additionalProperties":true,"description":"Absent or empty for most generic error codes."}}}]}}},"paths":{"/connect/webhooks/{webhookId}":{"delete":{"operationId":"deleteWebhookSubscription","tags":["Webhooks"],"summary":"Delete a webhook subscription.","description":"Soft-deletes a webhook subscription owned by the authenticated API app. Requires webhooks:write scope. The subscription stops receiving events, no longer counts toward the app's subscription limit, and its name can be reused. Deliveries still queued for it are cancelled. A subscription that does not exist, is already deleted, or belongs to another app returns 404.","parameters":[{"$ref":"#/components/parameters/ApiDateHeader"},{"$ref":"#/components/parameters/WebhookId"}],"responses":{"204":{"$ref":"#/components/responses/NoContent"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}}}}
```

## Send a webhook test event.

> Queues a test webhook event and returns delivery status. Never includes signing\_secret. The subscription must be active and event\_type must be in the subscription's event\_types list.<br>

```json
{"openapi":"3.1.0","info":{"title":"Medtrics Connect API","version":"2026-10-27"},"tags":[{"name":"Webhooks","description":"Webhook subscriptions, event delivery history, and test delivery for API app integrations."}],"servers":[{"url":"https://sandbox.mymedtrics.com","description":"Client sandbox — shared demo tenant, X-Medtrics-Sandbox-Key or a sandbox API app"},{"url":"https://{client}.mymedtrics.com","description":"Your Medtrics instance","variables":{"client":{"default":"<client-name>","description":"The tenant subdomain Medtrics assigns to your institution"}}}],"security":[{"BearerToken":[]},{"SandboxApiKey":[]}],"components":{"securitySchemes":{"BearerToken":{"type":"http","scheme":"bearer"},"SandboxApiKey":{"type":"apiKey","in":"header","name":"X-Medtrics-Sandbox-Key"}},"parameters":{"ApiDateHeader":{"name":"Medtrics-API-Date","in":"header","required":true,"description":"API contract date. Use 2026-10-27 for this version. Contract dates select request behavior: rotation creates require explicit placement references from 2026-10-27; earlier accepted dates retain compatibility handling. Unknown dates return HTTP 400 with error.code unsupported_api_date.","schema":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"}},"WebhookId":{"name":"webhookId","in":"path","required":true,"schema":{"type":"string"},"description":"Public ID of the webhook subscription (wh_ prefix). Backed by api_webhook_subscriptions.id."}},"schemas":{"WebhookEventType":{"type":"string","description":"Approved event type for webhook subscriptions.","enum":["program.updated","learner.updated","rotation.assignment_changed","evaluation.completed","import_job.completed"]},"WebhookDelivery":{"type":"object","description":"A single webhook delivery attempt record. Never includes signing_secret.","required":["id","webhook_id","event_type","status","attempt_count","created_at"],"properties":{"id":{"type":"string","description":"Public ID of the delivery record (deliv_ prefix)."},"webhook_id":{"type":"string","description":"Public ID of the parent webhook subscription (wh_ prefix)."},"event_type":{"$ref":"#/components/schemas/WebhookEventType"},"status":{"$ref":"#/components/schemas/WebhookDeliveryStatus"},"attempt_count":{"type":"integer"},"next_attempt_at":{"type":["string","null"],"format":"date-time"},"created_at":{"type":"string","format":"date-time"}}},"WebhookDeliveryStatus":{"type":"string","description":"Delivery attempt status. Canonical source is the api_webhook_delivery_status DB enum.","enum":["pending","sent","failed","retrying","disabled"]},"ErrorEnvelope":{"type":"object","required":["error"],"description":"Structured error envelope. error.code is the machine-readable discriminant. Typed detail schemas are provided for codes where structured data aids client remediation. Use the discriminator.mapping to select the correct variant schema; for all other codes see GenericApiError.\n","properties":{"error":{"anyOf":[{"$ref":"#/components/schemas/InvalidStatusError"},{"$ref":"#/components/schemas/InvalidWebhookEventsError"},{"$ref":"#/components/schemas/RateLimitedError"},{"$ref":"#/components/schemas/CursorResourceMismatchError"},{"$ref":"#/components/schemas/InvalidIdError"},{"$ref":"#/components/schemas/UnsupportedApiDateError"},{"$ref":"#/components/schemas/InactiveClientError"},{"$ref":"#/components/schemas/InvalidWebhookUrlError"},{"$ref":"#/components/schemas/GenericApiError"}],"discriminator":{"propertyName":"code","mapping":{"invalid_status":"#/components/schemas/InvalidStatusError","invalid_webhook_events":"#/components/schemas/InvalidWebhookEventsError","rate_limited":"#/components/schemas/RateLimitedError","cursor_resource_mismatch":"#/components/schemas/CursorResourceMismatchError","invalid_id":"#/components/schemas/InvalidIdError","unsupported_api_date":"#/components/schemas/UnsupportedApiDateError","inactive_client":"#/components/schemas/InactiveClientError","invalid_webhook_url":"#/components/schemas/InvalidWebhookUrlError"}}}}},"InvalidStatusError":{"description":"Returned when a status query parameter value is outside the allowed enum. HTTP 400. details.field identifies the parameter; details.allowed lists every accepted value.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_status"},"details":{"type":"object","required":["field","allowed"],"additionalProperties":false,"description":"Structured remediation data for invalid_status.","properties":{"field":{"type":"string","description":"Query parameter name that received the invalid value (e.g. enrollment_status, status).\n"},"allowed":{"type":"array","items":{"type":"string"},"description":"Full list of accepted values for the parameter."}}}}}]},"ApiErrorBase":{"type":"object","required":["code","message","request_id"],"description":"Common fields shared by every Connect API error response.","properties":{"code":{"type":"string","description":"Machine-readable error code."},"message":{"type":"string","description":"Human-readable error description."},"request_id":{"type":"string","description":"Request correlation ID. Include this value when reporting issues to support alongside the Medtrics-Request-ID response header.\n"}}},"InvalidWebhookEventsError":{"description":"Returned when event_types contains values not in the approved Connect API event list, or when event_types is empty. HTTP 400. details.unsupported_events lists the rejected entries (empty array when event_types was missing or entirely empty).\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_webhook_events"},"details":{"type":"object","required":["unsupported_events"],"additionalProperties":false,"description":"Structured remediation data for invalid_webhook_events.","properties":{"unsupported_events":{"type":"array","items":{"type":"string"},"description":"Event type strings that are not approved Connect API event names. Empty array indicates event_types was missing or contained only invalid entries.\n"}}}}}]},"RateLimitedError":{"description":"Returned when the request or API app rate limit is exceeded. HTTP 429. details.retry_after_seconds indicates the wait duration before retrying.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"rate_limited"},"details":{"type":"object","required":["retry_after_seconds"],"additionalProperties":false,"description":"Structured remediation data for rate_limited.","properties":{"retry_after_seconds":{"type":"integer","description":"Seconds to wait before retrying. Mirrors the Retry-After response header value.\n"}}}}}]},"CursorResourceMismatchError":{"description":"Returned when the cursor was issued for a different resource type than the endpoint being called. HTTP 400. details.expected_resource is the resource this endpoint serves; details.cursor_resource is what the cursor encodes.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"cursor_resource_mismatch"},"details":{"type":"object","required":["expected_resource","cursor_resource"],"additionalProperties":false,"description":"Structured remediation data for cursor_resource_mismatch.","properties":{"expected_resource":{"type":"string","description":"Resource type this endpoint serves (e.g. programs, learners)."},"cursor_resource":{"type":"string","description":"Resource type encoded in the submitted cursor."}}}}}]},"InvalidIdError":{"description":"Returned when a path parameter public ID does not match the expected prefix for the resource. HTTP 400. details.field is the parameter name; details.expected_prefix is the required ID prefix.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"invalid_id"},"details":{"type":"object","required":["field","expected_prefix"],"additionalProperties":false,"description":"Structured remediation data for invalid_id.","properties":{"field":{"type":"string","description":"Path or query parameter name that received the invalid ID."},"expected_prefix":{"type":"string","description":"Required ID prefix for this parameter."}}}}}]},"UnsupportedApiDateError":{"description":"Returned when the Medtrics-API-Date header is missing, malformed (not YYYY-MM-DD), or not a supported contract date. HTTP 400. details.supported_dates lists all accepted values. details.reason is one of \"missing\", \"malformed\", or \"not_supported\".\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","required":["details"],"properties":{"code":{"type":"string","const":"unsupported_api_date"},"details":{"type":"object","required":["reason","supported_dates"],"additionalProperties":false,"description":"Structured remediation data for unsupported_api_date.","properties":{"reason":{"type":"string","enum":["missing","malformed","not_supported"],"description":"Specific cause: \"missing\" when the header is absent, \"malformed\" when the value is not YYYY-MM-DD format, \"not_supported\" when the format is valid but the date is not a known contract date.\n"},"supported_dates":{"type":"array","items":{"type":"string"},"description":"All accepted Medtrics-API-Date values."}}}}}]},"InactiveClientError":{"description":"Returned when the authenticated API client is revoked, expired, deleted, or in a non-active status. HTTP 403. No structured details beyond the human-readable message.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"code":{"type":"string","const":"inactive_client"}}}]},"InvalidWebhookUrlError":{"description":"Returned when target_url fails HTTPS validation or is otherwise rejected. HTTP 422. No structured details field beyond the human-readable message.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"code":{"type":"string","const":"invalid_webhook_url"}}}]},"GenericApiError":{"description":"Catch-all error variant for codes that carry no structured remediation data (invalid_cursor, not_found, invalid_credentials, scope_not_allowed, invalid_webhook_tolerance, internal_error, and any future codes not covered by a typed variant in the discriminator.mapping). details is absent or empty for these codes.\n","allOf":[{"$ref":"#/components/schemas/ApiErrorBase"},{"type":"object","properties":{"details":{"type":"object","additionalProperties":true,"description":"Absent or empty for most generic error codes."}}}]}},"headers":{"MedtricsRequestId":{"description":"Correlates all gateway logs for the request.","schema":{"type":"string"}},"MedtricsApiDate":{"description":"API contract date applied to the request.","schema":{"type":"string"}},"RateLimitLimit":{"description":"Request limit for the current window.","schema":{"type":"string"}},"RateLimitRemaining":{"description":"Remaining requests in the current window.","schema":{"type":"string"}},"RateLimitReset":{"description":"Unix seconds when the current rate-limit window resets.","schema":{"type":"string"}}},"responses":{"BadRequest":{"description":"Invalid request parameters.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Unauthorized":{"description":"Missing or invalid credentials.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"Forbidden":{"description":"Credential lacks tenant visibility or required scope.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"RateLimited":{"description":"Request or API app rate limit exceeded.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"InternalError":{"description":"Unexpected server error.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"paths":{"/connect/webhooks/{webhookId}/test":{"post":{"operationId":"sendWebhookTest","tags":["Webhooks"],"summary":"Send a webhook test event.","description":"Queues a test webhook event and returns delivery status. Never includes signing_secret. The subscription must be active and event_type must be in the subscription's event_types list.\n","parameters":[{"$ref":"#/components/parameters/ApiDateHeader"},{"$ref":"#/components/parameters/WebhookId"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["event_type"],"properties":{"event_type":{"$ref":"#/components/schemas/WebhookEventType"}}}}}},"responses":{"202":{"description":"Test webhook delivery queued. Contains delivery status only — no signing_secret.","headers":{"Medtrics-Request-ID":{"$ref":"#/components/headers/MedtricsRequestId"},"Medtrics-API-Date":{"$ref":"#/components/headers/MedtricsApiDate"},"Medtrics-RateLimit-Limit":{"$ref":"#/components/headers/RateLimitLimit"},"Medtrics-RateLimit-Remaining":{"$ref":"#/components/headers/RateLimitRemaining"},"Medtrics-RateLimit-Reset":{"$ref":"#/components/headers/RateLimitReset"}},"content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/WebhookDelivery"}}}}}},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/InternalError"}}}}}}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.mymedtrics.com/api-reference/connect-api/webhooks.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
