{
  "openapi": "3.1.0",
  "info": {
    "title": "Credential Offers API",
    "description": "Create and manage credential offers.\n\nUse this API to create credential offers that can be used to issue verifiable credentials.",
    "version": "1.0.0",
    "x-metadata": {
      "title": "Credential Offers API",
      "description": "Create and manage credential offers"
    }
  },
  "servers": [
    {
      "url": "https://api.dev.deepcredentials.swiss",
      "description": "Development"
    },
    {
      "url": "https://api.int.deepcredentials.swiss",
      "description": "Integration"
    }
  ],
  "tags": [
    {
      "name": "Credential Offers",
      "description": "Create and manage credential offers."
    }
  ],
  "paths": {
    "/b2b/v1/credential-offers": {
      "get": {
        "description": "Returns a paginated list of credential offers for the calling service user's organization (X-Org-Id) and environment (X-Environment).\n",
        "operationId": "GET:b2b.ListCredentialOffers",
        "parameters": [
          {
            "$ref": "#/components/parameters/XOrgIdHeader"
          },
          {
            "$ref": "#/components/parameters/XEnvironmentHeader"
          },
          {
            "allowEmptyValue": true,
            "description": "Maximum number of results to return (1-100, default 20).\n",
            "explode": true,
            "in": "query",
            "name": "limit",
            "schema": {
              "format": "int64",
              "type": "integer"
            },
            "style": "form"
          },
          {
            "allowEmptyValue": true,
            "description": "Number of results to skip for pagination.\n",
            "explode": true,
            "in": "query",
            "name": "offset",
            "schema": {
              "format": "int64",
              "type": "integer"
            },
            "style": "form"
          },
          {
            "allowEmptyValue": true,
            "description": "Filter by offer status: pending, issued, accepted, expired, failed.\n",
            "explode": true,
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string"
            },
            "style": "form"
          },
          {
            "allowEmptyValue": true,
            "description": "Filter by credential definition ID (UUID).\n",
            "explode": true,
            "in": "query",
            "name": "credential_definition_id",
            "schema": {
              "type": "string"
            },
            "style": "form"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "offers": {
                      "items": {
                        "$ref": "#/components/schemas/b2b.b2bOfferResponse"
                      },
                      "description": "Page of offers matching the list filters.",
                      "type": "array"
                    },
                    "total": {
                      "format": "int64",
                      "description": "Total count matching filters (for pagination UI).",
                      "type": "integer"
                    }
                  },
                  "required": [
                    "offers",
                    "total"
                  ],
                  "type": "object"
                },
                "example": {
                  "offers": [
                    {
                      "id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
                      "environment": "beta",
                      "credential_definition": {
                        "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                        "vct": "person-id-vct",
                        "name": "Person ID"
                      },
                      "status": "issued",
                      "claims": {
                        "given_name": "Max",
                        "family_name": "Mustermann",
                        "date_of_birth": "1990-01-15",
                        "is_over_18": true
                      },
                      "reference_id": "order-12345",
                      "issued_credential_id": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
                      "expires_at": "2026-03-29T12:10:00Z",
                      "created_at": "2026-03-29T12:00:00Z",
                      "completed_at": "2026-03-29T12:01:30Z"
                    },
                    {
                      "id": "e5f6a7b8-c9d0-1234-5678-9abcdef01234",
                      "environment": "beta",
                      "credential_definition": {
                        "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                        "vct": "person-id-vct",
                        "name": "Person ID"
                      },
                      "status": "pending",
                      "deeplink": "openid-credential-offer://...",
                      "qr_content_base64": "iVBORw0KGgo...",
                      "claims": {
                        "given_name": "Anna",
                        "family_name": "Schmidt",
                        "date_of_birth": "1985-07-22",
                        "is_over_18": true
                      },
                      "expires_at": "2026-03-29T12:30:00Z",
                      "created_at": "2026-03-29T12:20:00Z"
                    }
                  ],
                  "total": 2
                }
              }
            },
            "description": "Success response"
          },
          "400": {
            "description": "Invalid request. Common causes: missing or malformed X-Org-Id / X-Environment headers, or a missing required field in the request body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "invalid_argument",
                  "message": "X-Org-Id header is required."
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated. The Bearer token failed validation (signature, issuer, audience, or expiry).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "unauthenticated",
                  "message": "Missing or invalid Bearer token."
                }
              }
            }
          },
          "403": {
            "description": "Permission denied. The token does not carry the scope required by this endpoint. Check the `scope` claim on the token you received from the DeepCloud SSO token endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "permission_denied",
                  "message": "Token lacks the required scope."
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "resource_exhausted",
                  "message": "Rate limit exceeded. Retry with backoff."
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "internal",
                  "message": "An unexpected error occurred."
                }
              }
            }
          }
        },
        "summary": "List credential offers.\n",
        "tags": [
          "Credential Offers"
        ]
      },
      "post": {
        "description": "Returns a deeplink and QR code for the wallet to scan. Optionally delivers the same offer out-of-band (currently: email).\n",
        "operationId": "POST:b2b.CreateCredentialOffer",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "properties": {
                  "claims": {
                    "description": "Claim values for the credential. Key-value object of claim data for the credential. Keys must match the claim names defined in the credential definition, and values must match the expected types (string, boolean, integer, number). All mandatory claims from the credential definition must be provided.",
                    "type": "object"
                  },
                  "credential_definition_id": {
                    "format": "uuid",
                    "description": "Credential definition to issue (UUID from the tenant's credential definition\ncatalog).",
                    "type": "string"
                  },
                  "delivery": {
                    "$ref": "#/components/schemas/b2b.DeliverySpec"
                  },
                  "offer_validity_seconds": {
                    "format": "int64",
                    "description": "Optional offer validity in seconds (default 600). This is the TTL of the QR/deeplink window before the wallet has to pick up the offer, not the lifetime of the credential itself.",
                    "type": "integer"
                  },
                  "reference_id": {
                    "description": "Optional correlation identifier from your system; echoed on the resource.",
                    "type": "string"
                  },
                  "validity_seconds": {
                    "format": "int64",
                    "description": "Optional credential lifetime in seconds (SD-JWT \"exp\" = iat + N). Omit to fall back to the credential definition's default\\_validity\\_seconds; when both are absent the credential is issued without an exp claim (never expires). Bounded by \\[60, 10 years].",
                    "type": "integer"
                  }
                },
                "required": [
                  "credential_definition_id",
                  "claims"
                ],
                "type": "object"
              },
              "example": {
                "credential_definition_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                "claims": {
                  "given_name": "Max",
                  "family_name": "Mustermann",
                  "date_of_birth": "1990-01-15",
                  "is_over_18": true
                },
                "reference_id": "order-12345",
                "offer_validity_seconds": 600
              }
            }
          }
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "properties": {
                    "claims": {
                      "description": "Claim values for the credential (echoed from the request).",
                      "type": "object"
                    },
                    "created_at": {
                      "format": "date-time",
                      "description": "When the offer was created (RFC3339).",
                      "type": "string"
                    },
                    "deeplink": {
                      "description": "OID4VCI wallet deep link (e.g. openid-credential-offer://...).",
                      "type": "string"
                    },
                    "expires_at": {
                      "format": "date-time",
                      "description": "Offer TTL deadline (RFC3339).",
                      "type": "string"
                    },
                    "id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "UUID v4 identifier.",
                      "example": "550e8400-e29b-41d4-a716-446655440000"
                    },
                    "qr_content_base64": {
                      "description": "Base64-encoded PNG; QR payload is the same URI as deeplink.",
                      "type": "string"
                    },
                    "status": {
                      "$ref": "#/components/schemas/issuer.OfferStatus"
                    }
                  },
                  "required": [
                    "id",
                    "status",
                    "deeplink",
                    "qr_content_base64",
                    "claims",
                    "expires_at",
                    "created_at"
                  ],
                  "type": "object"
                },
                "example": {
                  "id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
                  "status": "pending",
                  "deeplink": "openid-credential-offer://credential_offer?credential_issuer=https%3A%2F%2Fapi.dev.deepcredentials.swiss%2Foid4vci%2F550e8400-e29b-41d4-a716-446655440000%2Fbeta&credential_configuration_ids=PersonId&grants=%7B%22urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Apre-authorized_code%22%3A%7B%22pre-authorized_code%22%3A%22abc123%22%7D%7D",
                  "qr_content_base64": "iVBORw0KGgo...",
                  "claims": {
                    "given_name": "Max",
                    "family_name": "Mustermann",
                    "date_of_birth": "1990-01-15",
                    "is_over_18": true
                  },
                  "expires_at": "2026-03-29T12:10:00Z",
                  "created_at": "2026-03-29T12:00:00Z"
                }
              }
            },
            "description": "Success response"
          },
          "400": {
            "description": "Invalid request. Common causes: missing or malformed X-Org-Id / X-Environment headers, or a missing required field in the request body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "invalid_argument",
                  "message": "X-Org-Id header is required."
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated. The Bearer token failed validation (signature, issuer, audience, or expiry).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "unauthenticated",
                  "message": "Missing or invalid Bearer token."
                }
              }
            }
          },
          "403": {
            "description": "Permission denied. The token does not carry the scope required by this endpoint. Check the `scope` claim on the token you received from the DeepCloud SSO token endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "permission_denied",
                  "message": "Token lacks the required scope."
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "resource_exhausted",
                  "message": "Rate limit exceeded. Retry with backoff."
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "internal",
                  "message": "An unexpected error occurred."
                }
              }
            }
          }
        },
        "summary": "Create a credential offer.\n",
        "tags": [
          "Credential Offers"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/XOrgIdHeader"
          },
          {
            "$ref": "#/components/parameters/XEnvironmentHeader"
          }
        ]
      }
    },
    "/b2b/v1/credential-offers/{offerId}": {
      "get": {
        "description": "Returns the current status of a credential offer created via POST /b2b/v1/credential-offers. Poll this endpoint until the status is no longer \\`pending\\`.\n",
        "operationId": "GET:b2b.GetCredentialOffer",
        "parameters": [
          {
            "$ref": "#/components/parameters/XOrgIdHeader"
          },
          {
            "$ref": "#/components/parameters/XEnvironmentHeader"
          },
          {
            "allowEmptyValue": true,
            "explode": false,
            "in": "path",
            "name": "offerId",
            "required": true,
            "schema": {
              "format": "uuid",
              "type": "string"
            },
            "style": "simple"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/b2b.b2bOfferResponse"
                },
                "example": {
                  "id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
                  "environment": "beta",
                  "credential_definition": {
                    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                    "vct": "person-id-vct",
                    "name": "Person ID"
                  },
                  "status": "issued",
                  "claims": {
                    "given_name": "Max",
                    "family_name": "Mustermann",
                    "date_of_birth": "1990-01-15",
                    "is_over_18": true
                  },
                  "reference_id": "order-12345",
                  "issued_credential_id": "f1e2d3c4-b5a6-7890-1234-567890abcdef",
                  "expires_at": "2026-03-29T12:10:00Z",
                  "created_at": "2026-03-29T12:00:00Z",
                  "completed_at": "2026-03-29T12:01:30Z"
                }
              }
            },
            "description": "Success response"
          },
          "400": {
            "description": "Invalid request. Common causes: missing or malformed X-Org-Id / X-Environment headers, or a missing required field in the request body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "invalid_argument",
                  "message": "X-Org-Id header is required."
                }
              }
            }
          },
          "401": {
            "description": "Unauthenticated. The Bearer token failed validation (signature, issuer, audience, or expiry).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "unauthenticated",
                  "message": "Missing or invalid Bearer token."
                }
              }
            }
          },
          "403": {
            "description": "Permission denied. The token does not carry the scope required by this endpoint. Check the `scope` claim on the token you received from the DeepCloud SSO token endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "permission_denied",
                  "message": "Token lacks the required scope."
                }
              }
            }
          },
          "404": {
            "description": "Resource not found. Either the resource ID is unknown, or the X-Org-Id you supplied is not an organization the authenticated service user is a member of.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "not_found",
                  "message": "Resource not found."
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "resource_exhausted",
                  "message": "Rate limit exceeded. Retry with backoff."
                }
              }
            }
          },
          "500": {
            "description": "Internal server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "code": "internal",
                  "message": "An unexpected error occurred."
                }
              }
            }
          }
        },
        "summary": "Get credential offer.\n",
        "tags": [
          "Credential Offers"
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "DeepAdmin-issued Keycloak access token obtained via the OAuth 2.0\nResource Owner Password Credentials grant against the DeepCloud SSO\nrealm. See the Service Users guide for the exchange flow."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Machine-readable error code."
          },
          "message": {
            "type": "string",
            "description": "Human-readable error description."
          },
          "details": {
            "type": "object",
            "description": "Optional structured data with additional context."
          }
        }
      },
      "b2b.b2bOfferResponse": {
        "properties": {
          "claims": {
            "description": "Claim values for the credential.",
            "type": "object"
          },
          "completed_at": {
            "format": "date-time",
            "description": "Set when the offer leaves pending (issued, failed, or expired).",
            "type": "string"
          },
          "created_at": {
            "format": "date-time",
            "description": "When the offer was created (RFC3339).",
            "type": "string"
          },
          "credential_definition": {
            "allOf": [
              {
                "$ref": "#/components/schemas/b2b.credentialDefinitionRef"
              }
            ],
            "description": "Credential definition reference."
          },
          "credential_validity_seconds": {
            "format": "int64",
            "description": "Credential lifetime to apply at issuance (SD-JWT exp = iat + N). Absent means the credential will be issued without an exp claim.",
            "type": "integer"
          },
          "deeplink": {
            "description": "Only when status is pending: OID4VCI wallet deep link.",
            "type": "string"
          },
          "environment": {
            "description": "Swiyu environment: \"beta\" or \"production\".",
            "type": "string"
          },
          "error_code": {
            "description": "Machine-readable failure reason when status is failed or expired.",
            "type": "string"
          },
          "error_description": {
            "description": "Human-readable error detail when status is failed or expired.",
            "type": "string"
          },
          "expires_at": {
            "format": "date-time",
            "description": "When a pending offer expires if not completed (RFC3339).",
            "type": "string"
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "550e8400-e29b-41d4-a716-446655440000",
            "description": "Offer id (matches the value returned at creation time). UUID v4 identifier."
          },
          "invitation_id": {
            "type": "string",
            "format": "uuid",
            "example": "550e8400-e29b-41d4-a716-446655440000",
            "description": "InvitationID / SelfServiceRequestID expose the upstream lineage anchors when this offer was created from one. nil for plain portal or B2B offers."
          },
          "issued_credential_id": {
            "type": "string",
            "format": "uuid",
            "example": "550e8400-e29b-41d4-a716-446655440000",
            "description": "Only when status is issued: ID of the issued credential. UUID v4 identifier."
          },
          "qr_content_base64": {
            "description": "Only when status is pending: Base64-encoded PNG QR code (240×240 px).",
            "type": "string"
          },
          "reference_id": {
            "description": "Your reference_id from the create request, if any.",
            "type": "string"
          },
          "self_service_request_id": {
            "type": "string",
            "format": "uuid",
            "description": "UUID v4 identifier.",
            "example": "550e8400-e29b-41d4-a716-446655440000"
          },
          "source": {
            "description": "Source records where this offer originated: \"portal\" (admin UI), \"b2b\" (API caller), \"invitation\" (redeemed email invitation), or \"self\\_service\" (admin approval of a public request). Stable at row level — captured at create and never mutated.",
            "type": "string"
          },
          "status": {
            "allOf": [
              {
                "$ref": "#/components/schemas/issuer.OfferStatus"
              }
            ],
            "description": "Current lifecycle state of the offer."
          }
        },
        "required": [
          "id",
          "environment",
          "credential_definition",
          "status",
          "claims",
          "expires_at",
          "created_at",
          "source"
        ],
        "description": "The full detail of a credential offer.",
        "type": "object"
      },
      "b2b.credentialDefinitionRef": {
        "properties": {
          "has_logo": {
            "description": "Whether the credential definition currently has an uploaded logo.",
            "type": "boolean"
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "550e8400-e29b-41d4-a716-446655440000",
            "description": "Credential definition UUID. UUID v4 identifier."
          },
          "name": {
            "description": "Human-readable credential definition name.",
            "type": "string"
          },
          "vct": {
            "description": "Verifiable credential type (e.g. \"urn:ietf:params:oauth:vct:PersonId\").",
            "type": "string"
          }
        },
        "required": [
          "id",
          "vct",
          "name",
          "has_logo"
        ],
        "description": "A compact reference to the credential definition.",
        "type": "object"
      },
      "b2b.DeliverySpec": {
        "properties": {
          "email": {
            "description": "Email recipient, required when via=email.",
            "type": "string"
          },
          "via": {
            "description": "\"email\" is the only accepted value today; any other value rejects.",
            "type": "string"
          }
        },
        "required": [
          "via",
          "email"
        ],
        "description": "DeliverySpec selects an out-of-band delivery method for a freshly-created credential offer. Today only \\`via=email\\` is supported; future modes (sms, push) slot in here without changing the endpoint signature. Email is validated up-front so a bad input fails before we spend the work of creating the offer.",
        "type": "object"
      },
      "issuer.OfferStatus": {
        "type": "string",
        "enum": [
          "pending",
          "issued",
          "accepted",
          "expired",
          "failed"
        ],
        "description": "Lifecycle state of a credential offer."
      }
    },
    "parameters": {
      "XOrgIdHeader": {
        "name": "X-Org-Id",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string",
          "format": "uuid"
        },
        "description": "Organization the call is acting on. The authenticated service user must be a member of this organization in DeepAdmin; otherwise the request returns 404."
      },
      "XEnvironmentHeader": {
        "name": "X-Environment",
        "in": "header",
        "required": true,
        "schema": {
          "type": "string",
          "enum": [
            "beta",
            "production"
          ]
        },
        "description": "Environment the call is scoped to. Resources are isolated per environment."
      }
    }
  },
  "security": [
    {
      "BearerAuth": []
    }
  ]
}