From 0cb9c43a4d797b59a80154976210402f37afe05b Mon Sep 17 00:00:00 2001 From: Kevin Verdieck Date: Wed, 10 Dec 2025 14:38:55 -0800 Subject: [PATCH 1/2] Add docs for the Compliance API Logs Platform # Conflicts: # registry.yaml --- authors.yaml | 5 + .../compliance_api/logs_platform.ipynb | 407 ++++++++++++++++++ registry.yaml | 14 +- 3 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 examples/chatgpt/compliance_api/logs_platform.ipynb diff --git a/authors.yaml b/authors.yaml index 92b89168e9..4e14d57d64 100644 --- a/authors.yaml +++ b/authors.yaml @@ -547,3 +547,8 @@ derrickchoi-openai: name: "Derrick Choi" website: "https://www.linkedin.com/in/derrickchoi/" avatar: "https://avatars.githubusercontent.com/u/211427900" + +kevinv-openai: + name: "Kevin Verdieck" + website: "https://www.linkedin.com/in/kevinverdieck/" + avatar: "https://avatars.githubusercontent.com/u/197816265?v=4" diff --git a/examples/chatgpt/compliance_api/logs_platform.ipynb b/examples/chatgpt/compliance_api/logs_platform.ipynb new file mode 100644 index 0000000000..a288d3d283 --- /dev/null +++ b/examples/chatgpt/compliance_api/logs_platform.ipynb @@ -0,0 +1,407 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Enterprise Compliance API: Logs Platform quickstart\n\n", + "Use this notebook to explore the Logs Platform endpoints from the Enterprise Compliance API for ChatGPT Enterprise, EDU, and ChatGPT for Teachers. The examples focus on listing and downloading immutable JSONL log files so you can ingest them into your SIEM or data lake.\n\n", + "- Help center overview: [Compliance API for ChatGPT Enterprise, EDU, and ChatGPT for Teachers](https://help.openai.com/en/articles/9261474-compliance-api-for-chatgpt-enterprise-edu-and-chatgpt-for-teachers)\n", + "- API reference: [`Compliance API Logs Platform`](https://chatgpt.com/admin/api-reference#tag/Compliance-API-Logs-Platform)\n" + ], + "id": "3c9bde9be51b8b78" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Prerequisites\n", + "\n", + "- An Enterprise Compliance API key exported as `COMPLIANCE_API_KEY`.\n", + "- The ChatGPT account ID or the API Platform Org ID for the principal in question.\n", + "- Specific requirements for your environment" + ], + "id": "18f32c1126f4e3ce" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Quickstart Scripts\n", + "\n", + "Provided below are functionally identical scripts - one for Unix-based and one for Windows-based environments.\n", + "These scripts give an example of how one could build an integration with the Compliance API to retrieve and process\n", + "log data for given event types and time ranges.\n", + "These scripts handle listing and paging through the available log files and downloading them - writing the output to stdout.\n", + "\n", + "Example invocations of these scripts are embedded in their help blocks - execute them with no arguments to see them." + ], + "id": "4388a0474fcabd7" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Option 1: Unix-based\n", + "\n", + "Prerequisites:\n", + "- Save the script locally as `download_compliance_files.sh` and mark it executable\n", + "- Make sure you have up-to-date `bash`, `curl`, `sed`, and `date` installed.\n", + "- Format the date you want to get every log `after` as an ISO 8601 string including timezone.\n", + "\n", + "Run the script akin to `./download_compliance_files.sh `" + ], + "id": "e6999916958ccbba" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "```bash\n", + "#!/usr/bin/env bash\n", + "set -euo pipefail\n", + "\n", + "usage() {\n", + " echo \"Usage: $0 \" >&2\n", + " echo >&2\n", + " echo 'Examples: ' >&2\n", + " echo 'COMPLIANCE_API_KEY= ./download_compliance_files.sh f7f33107-5fb9-4ee1-8922-3eae76b5b5a0 AUTH_LOG 100 \"$(date -u -v-1d +%Y-%m-%dT%H:%M:%SZ)\" > output.jsonl' >&2\n", + " echo 'COMPLIANCE_API_KEY= ./download_compliance_files.sh org-p13k3klgno5cqxbf0q8hpgrk AUTH_LOG 100 \"$(date -u -v-1d +%Y-%m-%dT%H:%M:%SZ)\" > output.jsonl' >&2\n", + "}\n", + "\n", + "if [[ $# -ne 4 ]]; then\n", + " usage\n", + " exit 2\n", + "fi\n", + "\n", + "PRINCIPAL_ID=\"$1\"\n", + "EVENT_TYPE=\"$2\"\n", + "LIMIT=\"$3\"\n", + "INITIAL_AFTER=\"$4\"\n", + "\n", + "# Require COMPLIANCE_API_KEY to be present and non-empty before using it\n", + "if [[ -z \"${COMPLIANCE_API_KEY:-}\" ]]; then\n", + " echo \"COMPLIANCE_API_KEY environment variable is required. e.g.:\" >&2\n", + " echo \"COMPLIANCE_API_KEY= $0 \" >&2\n", + " exit 2\n", + "fi\n", + "\n", + "API_BASE=\"https://api.chatgpt.com/v1/compliance\"\n", + "AUTH_HEADER=(\"-H\" \"Authorization: Bearer ${COMPLIANCE_API_KEY}\")\n", + "\n", + "# Determine whether the first arg is a workspace ID or an org ID.\n", + "# If it starts with \"org-\" treat it as an organization ID and switch the path segment accordingly.\n", + "SCOPE_SEGMENT=\"workspaces\"\n", + "if [[ \"${PRINCIPAL_ID}\" == org-* ]]; then\n", + " SCOPE_SEGMENT=\"organizations\"\n", + "fi\n", + "\n", + "# Perform a curl request and fail fast on HTTP errors, logging context to stderr.\n", + "# Usage: perform_curl \"description of action\" \n", + "perform_curl() {\n", + " local description=\"$1\"\n", + " shift\n", + " # Capture body and HTTP status code, keeping body on stdout-like var\n", + " # We append a newline before the status to reliably split even if body has no trailing newline.\n", + " local combined\n", + " if ! combined=$(curl -sS -w \"\\n%{http_code}\" \"$@\"); then\n", + " echo \"Network/transport error while ${description}\" >&2\n", + " exit 1\n", + " fi\n", + " local http_code\n", + " http_code=\"${combined##*$'\\n'}\"\n", + " local body\n", + " body=\"${combined%$'\\n'*}\"\n", + "\n", + " if [[ ! \"${http_code}\" =~ ^2[0-9][0-9]$ ]]; then\n", + " echo \"HTTP error ${http_code} while ${description}:\" >&2\n", + " if [[ -n \"${body}\" ]]; then\n", + " # Print the body to stderr so it doesn't corrupt stdout stream\n", + " echo \"${body}\" | jq . >&2\n", + " fi\n", + " exit 1\n", + " fi\n", + "\n", + " # On success, emit body to stdout for callers to consume\n", + " echo \"${body}\"\n", + "}\n", + "\n", + "list_logs() {\n", + " local after=\"$1\"\n", + " perform_curl \"listing logs (after=${after}, event_type=${EVENT_TYPE}, limit=${LIMIT})\" \\\n", + " -G \\\n", + " \"${API_BASE}/${SCOPE_SEGMENT}/${PRINCIPAL_ID}/logs\" \\\n", + " \"${AUTH_HEADER[@]}\" \\\n", + " --data-urlencode \"limit=${LIMIT}\" \\\n", + " --data-urlencode \"event_type=${EVENT_TYPE}\" \\\n", + " --data-urlencode \"after=${after}\"\n", + "}\n", + "\n", + "download_log() {\n", + " local id=\"$1\"\n", + " echo \"Fetching logs for ID: ${id}\" >&2\n", + " perform_curl \"downloading log id=${id}\" \\\n", + " -G -L \\\n", + " \"${API_BASE}/${SCOPE_SEGMENT}/${PRINCIPAL_ID}/logs/${id}\" \\\n", + " \"${AUTH_HEADER[@]}\"\n", + "}\n", + "\n", + "to_local_human() {\n", + " local iso=\"$1\"\n", + " if [[ -z \"${iso}\" || \"${iso}\" == \"null\" ]]; then\n", + " echo \"\"\n", + " return 0\n", + " fi\n", + "\n", + " local iso_norm\n", + " iso_norm=$(echo -n \"${iso}\" \\\n", + " | sed -E 's/\\.[0-9]+(Z|[+-][0-9:]+)$/\\1/' \\\n", + " | sed -E 's/([+-]00:00)$/Z/')\n", + "\n", + " # macOS/BSD date: parse UTC to epoch then format in local timezone\n", + " local epoch\n", + " epoch=$(date -j -u -f \"%Y-%m-%dT%H:%M:%SZ\" \"${iso_norm}\" +%s 2>/dev/null) || true\n", + " if [[ -n \"${epoch}\" ]]; then\n", + " date -r \"${epoch}\" \"+%Y-%m-%d %H:%M:%S %Z\" 2>/dev/null && return 0\n", + " fi\n", + "\n", + " # Fallback to original if parsing failed\n", + " echo \"${iso}\"\n", + "}\n", + "\n", + "current_after=\"${INITIAL_AFTER}\"\n", + "page=1\n", + "total_downloaded=0\n", + "while true; do\n", + " echo \"Fetching page ${page} with after='${current_after}' (local: $(to_local_human \"${current_after}\"))\" >&2\n", + " response_json=\"$(list_logs \"${current_after}\")\"\n", + "\n", + " # Count and download each ID from the current page (if any)\n", + " page_count=\"$(echo \"${response_json}\" | jq '.data | length')\"\n", + " if [[ \"${page_count}\" -gt 0 ]]; then\n", + " echo \"${response_json}\" | jq -r '.data[].id' | while read -r id; do\n", + " download_log \"${id}\"\n", + " done\n", + " total_downloaded=$((total_downloaded + page_count))\n", + " fi\n", + "\n", + " has_more=\"$(echo \"${response_json}\" | jq -r '.has_more')\"\n", + " current_after=\"$(echo \"${response_json}\" | jq -r '.last_end_time')\"\n", + " if [[ \"${has_more}\" == \"true\" ]]; then\n", + " page=$((page + 1))\n", + " else\n", + " break\n", + " fi\n", + "done\n", + "\n", + "if [[ \"${total_downloaded}\" -eq 0 && ( -z \"${current_after}\" || \"${current_after}\" == \"null\" ) ]]; then\n", + " echo \"No results found for event_type ${EVENT_TYPE} after ${INITIAL_AFTER}\" >&2\n", + "else\n", + " echo \"Completed downloading ${total_downloaded} log files up to ${current_after} (local: $(to_local_human \"${current_after}\"))\" >&2\n", + "fi\n", + "```" + ], + "id": "e6ed22a253b67850" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "## Option 2: Windows-based\n", + "\n", + "Prerequisites:\n", + "- Save the script locally as `download_compliance_files.ps1`\n", + "- Open PowerShell (Version 5.1+) and navigate to the directory where the script is saved.\n", + "\n", + "Run the script akin to `.\\download_compliance_files.ps1 `" + ], + "id": "206e977bd1b7a441" + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": [ + "```ps\n", + "#!/usr/bin/env pwsh\n", + "#Requires -Version 5.1\n", + "\n", + "Set-StrictMode -Version Latest\n", + "$ErrorActionPreference = 'Stop'\n", + "\n", + "Add-Type -AssemblyName System.Web\n", + "\n", + "function Show-Usage {\n", + " [Console]::Error.WriteLine(@\"\n", + "Usage: .\\download_compliance_files.ps1 \n", + "\n", + "Example:\n", + " `$env:COMPLIANCE_API_KEY = ''\n", + " .\\download_compliance_files.ps1 f7f33107-5fb9-4ee1-8922-3eae76b5b5a0 AUTH_LOG 100 (Get-Date -AsUTC).AddDays(-1).ToString('yyyy-MM-ddTHH:mm:ssZ') |\n", + " Out-File -Encoding utf8 output.jsonl\n", + "\n", + "Example (org id):\n", + " `$env:COMPLIANCE_API_KEY = ''\n", + " .\\download_compliance_files.ps1 org-p13k3klgno5cqxbf0q8hpgrk AUTH_LOG 100 (Get-Date -AsUTC).AddDays(-1).ToString('yyyy-MM-ddTHH:mm:ssZ') |\n", + " Out-File -Encoding utf8 output.jsonl\n", + "\"@)\n", + "}\n", + "\n", + "if ($args.Count -ne 4) {\n", + " Show-Usage\n", + " exit 2\n", + "}\n", + "\n", + "if (-not $env:COMPLIANCE_API_KEY) {\n", + " [Console]::Error.WriteLine('COMPLIANCE_API_KEY environment variable must be set.')\n", + " exit 2\n", + "}\n", + "\n", + "$PrincipalId = $args[0]\n", + "$EventType = $args[1]\n", + "$Limit = $args[2]\n", + "$InitialAfter = $args[3]\n", + "\n", + "$ApiBase = 'https://api.chatgpt.com/v1/compliance'\n", + "\n", + "if ($PrincipalId.StartsWith('org-')) {\n", + " $ScopeSegment = 'organizations'\n", + "} else {\n", + " $ScopeSegment = 'workspaces'\n", + "}\n", + "\n", + "$handler = [System.Net.Http.HttpClientHandler]::new()\n", + "$client = [System.Net.Http.HttpClient]::new($handler)\n", + "$client.DefaultRequestHeaders.Authorization = New-Object System.Net.Http.Headers.AuthenticationHeaderValue('Bearer', $env:COMPLIANCE_API_KEY)\n", + "\n", + "function Invoke-ComplianceRequest {\n", + " param(\n", + " [Parameter(Mandatory = $true)] [string] $Description,\n", + " [Parameter(Mandatory = $true)] [string] $Path,\n", + " [hashtable] $Query = @{}\n", + " )\n", + "\n", + " $builder = [System.UriBuilder]::new(\"$ApiBase/$ScopeSegment/$PrincipalId/$Path\")\n", + " $queryString = [System.Web.HttpUtility]::ParseQueryString($builder.Query)\n", + " foreach ($key in $Query.Keys) {\n", + " $queryString[$key] = $Query[$key]\n", + " }\n", + " $builder.Query = $queryString.ToString()\n", + "\n", + " try {\n", + " $response = $client.GetAsync($builder.Uri).GetAwaiter().GetResult()\n", + " } catch {\n", + " [Console]::Error.WriteLine(\"Network/transport error while $Description\")\n", + " exit 1\n", + " }\n", + "\n", + " $body = $response.Content.ReadAsStringAsync().GetAwaiter().GetResult()\n", + " if (-not $response.IsSuccessStatusCode) {\n", + " [Console]::Error.WriteLine(\"HTTP error $($response.StatusCode.value__) while ${Description}:\")\n", + " if ($body) {\n", + " try {\n", + " $parsed = $body | ConvertFrom-Json\n", + " $parsed | ConvertTo-Json -Depth 10 | Write-Error\n", + " } catch {\n", + " [Console]::Error.WriteLine($body)\n", + " }\n", + " }\n", + " exit 1\n", + " }\n", + "\n", + " Write-Output $body\n", + "}\n", + "\n", + "function List-Logs {\n", + " param(\n", + " [Parameter(Mandatory = $true)] [string] $After\n", + " )\n", + "\n", + " Invoke-ComplianceRequest -Description \"listing logs (after=$After, event_type=$EventType, limit=$Limit)\" -Path 'logs' -Query @{\n", + " limit = $Limit\n", + " event_type = $EventType\n", + " after = $After\n", + " }\n", + "}\n", + "\n", + "function Download-Log {\n", + " param(\n", + " [Parameter(Mandatory = $true)] [string] $Id\n", + " )\n", + "\n", + " [Console]::Error.WriteLine(\"Fetching logs for ID: $Id\")\n", + " Invoke-ComplianceRequest -Description \"downloading log id=$Id\" -Path \"logs/$Id\"\n", + "}\n", + "\n", + "function ConvertTo-LocalHuman {\n", + " param(\n", + " [string] $Iso\n", + " )\n", + "\n", + " if (-not $Iso -or $Iso -eq 'null') {\n", + " return ''\n", + " }\n", + "\n", + " try {\n", + " $dt = [datetimeoffset]::Parse($Iso)\n", + " return $dt.ToLocalTime().ToString('yyyy-MM-dd HH:mm:ss zzz')\n", + " } catch {\n", + " return $Iso\n", + " }\n", + "}\n", + "\n", + "$currentAfter = $InitialAfter\n", + "$page = 1\n", + "$totalDownloaded = 0\n", + "while ($true) {\n", + " [Console]::Error.WriteLine(\"Fetching page $page with after='$currentAfter' (local: $(ConvertTo-LocalHuman -Iso $currentAfter))\")\n", + " $responseJson = List-Logs -After $currentAfter\n", + " $responseObj = $responseJson | ConvertFrom-Json\n", + "\n", + " $pageCount = $responseObj.data.Count\n", + " if ($pageCount -gt 0) {\n", + " foreach ($entry in $responseObj.data) {\n", + " Download-Log -Id $entry.id\n", + " }\n", + " $totalDownloaded += $pageCount\n", + " }\n", + "\n", + " $hasMore = $false\n", + " if ($null -ne $responseObj.has_more) {\n", + " $hasMore = [System.Convert]::ToBoolean($responseObj.has_more)\n", + " }\n", + "\n", + " $currentAfter = $responseObj.last_end_time\n", + " if ($hasMore) {\n", + " $page += 1\n", + " } else {\n", + " break\n", + " }\n", + "}\n", + "\n", + "if ($totalDownloaded -eq 0 -and ([string]::IsNullOrEmpty($currentAfter) -or $currentAfter -eq 'null')) {\n", + " [Console]::Error.WriteLine(\"No results found for event_type $EventType after $InitialAfter\")\n", + "} else {\n", + " [Console]::Error.WriteLine(\"Completed downloading $totalDownloaded log files up to $currentAfter (local: $(ConvertTo-LocalHuman -Iso $currentAfter))\")\n", + "}\n", + "\n", + "$client.Dispose()\n", + "$handler.Dispose()\n", + "```" + ], + "id": "2a26163e0617856a" + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/registry.yaml b/registry.yaml index 049a10df0e..530892dc2e 100644 --- a/registry.yaml +++ b/registry.yaml @@ -2681,11 +2681,23 @@ tags: - codex +- title: Enterprise Compliance API Logs Platform quickstart + path: examples/chatgpt/compliance_api/logs_platform.ipynb + date: 2025-12-11 + authors: + - kevinv-openai + tags: + - chatgpt + - chatgpt-data + - chatgpt-and-api + - compliance + - enterprise + - title: GPT-5.2 Prompting Guide path: examples/gpt-5/gpt-5-2_prompting_guide.ipynb date: 2025-12-11 authors: - msingh-openai - - emre-openai + - emre-openai tags: - gpt-5.2 From 924c22b9ac64b977c9dbedf09256b7cfd19bfc79 Mon Sep 17 00:00:00 2001 From: Kevin Verdieck Date: Wed, 10 Dec 2025 19:28:44 -0800 Subject: [PATCH 2/2] update for new naming --- examples/chatgpt/compliance_api/logs_platform.ipynb | 10 ++++++---- registry.yaml | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/chatgpt/compliance_api/logs_platform.ipynb b/examples/chatgpt/compliance_api/logs_platform.ipynb index a288d3d283..d51eb731c4 100644 --- a/examples/chatgpt/compliance_api/logs_platform.ipynb +++ b/examples/chatgpt/compliance_api/logs_platform.ipynb @@ -4,10 +4,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Enterprise Compliance API: Logs Platform quickstart\n\n", - "Use this notebook to explore the Logs Platform endpoints from the Enterprise Compliance API for ChatGPT Enterprise, EDU, and ChatGPT for Teachers. The examples focus on listing and downloading immutable JSONL log files so you can ingest them into your SIEM or data lake.\n\n", - "- Help center overview: [Compliance API for ChatGPT Enterprise, EDU, and ChatGPT for Teachers](https://help.openai.com/en/articles/9261474-compliance-api-for-chatgpt-enterprise-edu-and-chatgpt-for-teachers)\n", - "- API reference: [`Compliance API Logs Platform`](https://chatgpt.com/admin/api-reference#tag/Compliance-API-Logs-Platform)\n" + "# OpenAI Compliance Logs Platform quickstart\n", + "\n", + "Use this notebook to get started using the OpenAI Compliance Logs Platform. The examples focus on downloading log files so you can ingest them into your SIEM or data lake.\n", + "\n", + "- [Help Center Overview](https://help.openai.com/en/articles/9261474-compliance-api-for-chatgpt-enterprise-edu-and-chatgpt-for-teachers)\n", + "- [API Reference](https://chatgpt.com/admin/api-reference#tag/Compliance-API-Logs-Platform)\n" ], "id": "3c9bde9be51b8b78" }, diff --git a/registry.yaml b/registry.yaml index 530892dc2e..7c080a01ad 100644 --- a/registry.yaml +++ b/registry.yaml @@ -2681,7 +2681,7 @@ tags: - codex -- title: Enterprise Compliance API Logs Platform quickstart +- title: OpenAI Compliance Logs Platform quickstart path: examples/chatgpt/compliance_api/logs_platform.ipynb date: 2025-12-11 authors: