openapi: 3.0.3 info: title: SentryAgent.ai — Credential Management Service version: 1.0.0 description: | The Credential Management Service provides secure generation, listing, rotation, and revocation of OAuth 2.0 client credentials for registered AI agents. Each agent can hold multiple credentials simultaneously to support zero-downtime rotation. A credential consists of a `client_id` (= `agentId`) and a `client_secret`. **Security model**: - `client_secret` is returned **once only** — at creation or rotation time. - Secrets are stored as a bcrypt hash; plain-text is never persisted. - An agent may only manage its own credentials unless the caller holds an admin-scoped token. - Rotating a credential immediately revokes the previous `client_secret`. **Auth**: Bearer token (JWT) required on all endpoints. servers: - url: http://localhost:3000/api/v1 description: Local development server - url: https://api.sentryagent.ai/v1 description: Production server tags: - name: Credential Management description: Generate, list, rotate, and revoke agent credentials components: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT description: | JWT access token obtained via `POST /token`. Include as: `Authorization: Bearer ` schemas: CredentialStatus: type: string description: | Lifecycle status of a credential. - `active`: Credential is valid and can be used to obtain tokens. - `revoked`: Credential has been explicitly revoked and is permanently invalid. enum: - active - revoked example: active Credential: type: object description: | A credential record for an AI agent. The `clientSecret` is **never** returned in this schema — it is only returned once in `CredentialWithSecret` at the moment of creation or rotation. required: - credentialId - clientId - status - createdAt properties: credentialId: type: string format: uuid description: Immutable, system-assigned unique identifier for this credential. readOnly: true example: "c9d8e7f6-a5b4-3210-fedc-ba9876543210" clientId: type: string format: uuid description: > The `agentId` this credential belongs to. Equal to the `agentId` path parameter. readOnly: true example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" status: $ref: '#/components/schemas/CredentialStatus' createdAt: type: string format: date-time description: ISO 8601 timestamp when the credential was created. readOnly: true example: "2026-03-28T09:00:00.000Z" expiresAt: type: string format: date-time nullable: true description: | ISO 8601 timestamp when the credential expires. `null` indicates the credential does not expire (valid until revoked). example: "2027-03-28T09:00:00.000Z" revokedAt: type: string format: date-time nullable: true description: > ISO 8601 timestamp when the credential was revoked. `null` if the credential has not been revoked. readOnly: true example: null CredentialWithSecret: allOf: - $ref: '#/components/schemas/Credential' - type: object description: | Extended credential record returned **only** at creation or rotation time. The `clientSecret` is shown once and never retrievable again. Store it securely immediately. required: - clientSecret properties: clientSecret: type: string description: | The plain-text client secret. **Shown once only** — store securely immediately. This value is not persisted in plain text on the server. format: password example: "sk_live_7f3a2b1c9d8e4f0a6b5c3d2e1f0a9b8c" GenerateCredentialRequest: type: object description: | Optional request body for generating new credentials. If `expiresAt` is omitted, the credential does not expire. properties: expiresAt: type: string format: date-time description: | Optional ISO 8601 expiry timestamp for the credential. Must be a future date. If omitted, the credential has no expiry. example: "2027-03-28T09:00:00.000Z" PaginatedCredentialsResponse: type: object description: Paginated list of credentials for an agent. required: - data - total - page - limit properties: data: type: array items: $ref: '#/components/schemas/Credential' total: type: integer description: Total number of credentials for this agent. example: 3 page: type: integer description: Current page number (1-based). example: 1 limit: type: integer description: Number of items per page. example: 20 ErrorResponse: type: object description: Standard error response envelope. required: - code - message properties: code: type: string description: Machine-readable error code. example: "CREDENTIAL_NOT_FOUND" message: type: string description: Human-readable description of the error. example: "Credential with the specified ID was not found." details: type: object description: Optional structured details about the error. additionalProperties: true example: {} responses: Unauthorized: description: Missing or invalid Bearer token. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "UNAUTHORIZED" message: "A valid Bearer token is required to access this resource." Forbidden: description: Valid token but insufficient permissions. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "FORBIDDEN" message: "You do not have permission to manage credentials for this agent." AgentNotFound: description: The specified agent does not exist. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "AGENT_NOT_FOUND" message: "Agent with the specified ID was not found." CredentialNotFound: description: The specified credential does not exist. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "CREDENTIAL_NOT_FOUND" message: "Credential with the specified ID was not found." TooManyRequests: description: Rate limit exceeded. headers: X-RateLimit-Limit: schema: type: integer example: 100 X-RateLimit-Remaining: schema: type: integer example: 0 X-RateLimit-Reset: schema: type: integer example: 1743155400 content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "RATE_LIMIT_EXCEEDED" message: "Too many requests. Please retry after the rate limit window resets." InternalServerError: description: Unexpected server error. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "INTERNAL_SERVER_ERROR" message: "An unexpected error occurred. Please try again later." security: - BearerAuth: [] paths: /agents/{agentId}/credentials: parameters: - name: agentId in: path description: The unique UUID identifier of the agent. required: true schema: type: string format: uuid example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" post: operationId: generateCredential tags: - Credential Management summary: Generate new credentials for an agent description: | Generates a new `client_id` + `client_secret` credential pair for the specified agent. **Important**: The `clientSecret` is returned **once only** in this response. It is not stored in plain text on the server and cannot be retrieved later. Store it securely immediately (e.g. in a secrets manager). An agent may hold multiple active credentials simultaneously. This supports zero-downtime rotation: generate a new credential, update all consumers, then revoke the old one. **Restrictions**: - The agent must be in `active` status. - An agent may manage its own credentials via a self-issued token. - Managing another agent's credentials requires an admin-scoped token. requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/GenerateCredentialRequest' example: expiresAt: "2027-03-28T09:00:00.000Z" responses: '201': description: | Credential generated successfully. **Save the `clientSecret` immediately — it will not be shown again.** headers: X-RateLimit-Limit: schema: type: integer example: 100 X-RateLimit-Remaining: schema: type: integer example: 99 X-RateLimit-Reset: schema: type: integer example: 1743155400 content: application/json: schema: $ref: '#/components/schemas/CredentialWithSecret' example: credentialId: "c9d8e7f6-a5b4-3210-fedc-ba9876543210" clientId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" clientSecret: "sk_live_7f3a2b1c9d8e4f0a6b5c3d2e1f0a9b8c" status: "active" createdAt: "2026-03-28T09:00:00.000Z" expiresAt: "2027-03-28T09:00:00.000Z" revokedAt: null '400': description: Invalid request body. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "VALIDATION_ERROR" message: "Request validation failed." details: field: "expiresAt" reason: "expiresAt must be a future date-time." '401': $ref: '#/components/responses/Unauthorized' '403': description: Insufficient permissions or agent is not active. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: forbidden: summary: Insufficient permissions value: code: "FORBIDDEN" message: "You do not have permission to manage credentials for this agent." agentNotActive: summary: Agent not in active status value: code: "AGENT_NOT_ACTIVE" message: "Credentials can only be generated for active agents." details: agentId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" status: "suspended" '404': $ref: '#/components/responses/AgentNotFound' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalServerError' get: operationId: listCredentials tags: - Credential Management summary: List credentials for an agent description: | Returns a paginated list of all credentials (active and revoked) for the specified agent. The `clientSecret` is **never** returned in list responses. Results are ordered by `createdAt` descending (most recent first). parameters: - name: page in: query description: Page number (1-based). Defaults to `1`. required: false schema: type: integer minimum: 1 default: 1 example: 1 - name: limit in: query description: Number of results per page. Defaults to `20`, maximum `100`. required: false schema: type: integer minimum: 1 maximum: 100 default: 20 example: 20 - name: status in: query description: Filter credentials by status. required: false schema: $ref: '#/components/schemas/CredentialStatus' responses: '200': description: Credential list returned successfully. headers: X-RateLimit-Limit: schema: type: integer example: 100 X-RateLimit-Remaining: schema: type: integer example: 98 X-RateLimit-Reset: schema: type: integer example: 1743155400 content: application/json: schema: $ref: '#/components/schemas/PaginatedCredentialsResponse' example: data: - credentialId: "c9d8e7f6-a5b4-3210-fedc-ba9876543210" clientId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" status: "active" createdAt: "2026-03-28T09:00:00.000Z" expiresAt: "2027-03-28T09:00:00.000Z" revokedAt: null - credentialId: "d8e7f6a5-b4c3-2109-edcb-a98765432109" clientId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" status: "revoked" createdAt: "2026-01-15T08:00:00.000Z" expiresAt: null revokedAt: "2026-03-28T08:59:00.000Z" total: 2 page: 1 limit: 20 '400': description: Invalid query parameters. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "VALIDATION_ERROR" message: "Invalid query parameter value." details: field: "status" reason: "Must be 'active' or 'revoked'." '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/AgentNotFound' '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalServerError' /agents/{agentId}/credentials/{credentialId}/rotate: parameters: - name: agentId in: path description: The unique UUID identifier of the agent. required: true schema: type: string format: uuid example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" - name: credentialId in: path description: The unique UUID identifier of the credential to rotate. required: true schema: type: string format: uuid example: "c9d8e7f6-a5b4-3210-fedc-ba9876543210" post: operationId: rotateCredential tags: - Credential Management summary: Rotate a credential description: | Rotates an existing credential by: 1. Immediately revoking the current `client_secret`. 2. Generating and returning a new `client_secret` for the same `credentialId`. The `credentialId` remains the same after rotation; only the secret changes. The new `clientSecret` is returned **once only** and must be stored securely. **Use case**: Periodic secret rotation or emergency rotation after credential compromise. Only `active` credentials can be rotated. Attempting to rotate a `revoked` credential returns `409 Conflict`. requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/GenerateCredentialRequest' example: expiresAt: "2028-03-28T09:00:00.000Z" responses: '200': description: | Credential rotated successfully. **Save the new `clientSecret` immediately — it will not be shown again.** The previous secret is permanently invalidated. headers: X-RateLimit-Limit: schema: type: integer example: 100 X-RateLimit-Remaining: schema: type: integer example: 97 X-RateLimit-Reset: schema: type: integer example: 1743155400 content: application/json: schema: $ref: '#/components/schemas/CredentialWithSecret' example: credentialId: "c9d8e7f6-a5b4-3210-fedc-ba9876543210" clientId: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" clientSecret: "sk_live_9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d" status: "active" createdAt: "2026-03-28T09:00:00.000Z" expiresAt: "2028-03-28T09:00:00.000Z" revokedAt: null '400': description: Invalid request body. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "VALIDATION_ERROR" message: "Request validation failed." details: field: "expiresAt" reason: "expiresAt must be a future date-time." '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': description: Agent or credential not found. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: agentNotFound: summary: Agent not found value: code: "AGENT_NOT_FOUND" message: "Agent with the specified ID was not found." credentialNotFound: summary: Credential not found value: code: "CREDENTIAL_NOT_FOUND" message: "Credential with the specified ID was not found." '409': description: Credential is already revoked and cannot be rotated. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "CREDENTIAL_ALREADY_REVOKED" message: "Revoked credentials cannot be rotated. Generate a new credential instead." details: credentialId: "c9d8e7f6-a5b4-3210-fedc-ba9876543210" revokedAt: "2026-03-20T10:00:00.000Z" '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalServerError' /agents/{agentId}/credentials/{credentialId}: parameters: - name: agentId in: path description: The unique UUID identifier of the agent. required: true schema: type: string format: uuid example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" - name: credentialId in: path description: The unique UUID identifier of the credential to revoke. required: true schema: type: string format: uuid example: "c9d8e7f6-a5b4-3210-fedc-ba9876543210" delete: operationId: revokeCredential tags: - Credential Management summary: Revoke a credential description: | Permanently revokes a credential, immediately preventing it from being used to obtain new tokens. **Effects of revocation**: - The credential's status is set to `revoked`. - Any tokens issued using this credential remain valid until they expire naturally (token revocation is handled separately via `POST /token/revoke`). - The credential record is retained for audit purposes. - This operation is **irreversible** — a revoked credential cannot be re-activated. Revoking an already-revoked credential returns `409 Conflict`. responses: '204': description: Credential revoked successfully. No response body. headers: X-RateLimit-Limit: schema: type: integer example: 100 X-RateLimit-Remaining: schema: type: integer example: 96 X-RateLimit-Reset: schema: type: integer example: 1743155400 '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': description: Agent or credential not found. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' examples: agentNotFound: summary: Agent not found value: code: "AGENT_NOT_FOUND" message: "Agent with the specified ID was not found." credentialNotFound: summary: Credential not found value: code: "CREDENTIAL_NOT_FOUND" message: "Credential with the specified ID was not found." '409': description: Credential is already revoked. content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' example: code: "CREDENTIAL_ALREADY_REVOKED" message: "This credential has already been revoked." details: credentialId: "c9d8e7f6-a5b4-3210-fedc-ba9876543210" revokedAt: "2026-03-20T10:00:00.000Z" '429': $ref: '#/components/responses/TooManyRequests' '500': $ref: '#/components/responses/InternalServerError'