{
"cells": [
{
"cell_type": "markdown",
"id": "a1b2c3d4",
"metadata": {},
"source": [
"# GHG Audit Export"
]
},
{
"cell_type": "markdown",
"id": "b2c3d4e5",
"metadata": {},
"source": [
"**Table of contents**\n",
"- Overview\n",
"\n",
"- Setup\n",
"\n",
" - Authentication Token\n",
"\n",
"- Trigger Audit Export\n",
"\n",
" - Output Description\n",
"\n",
"- Get Audit Export Status\n",
"\n",
" - Output Description\n",
"\n",
"- Download Audit Export\n",
"\n",
"- Related Links"
]
},
{
"cell_type": "markdown",
"id": "c3d4e5f6",
"metadata": {},
"source": [
"## Overview\n",
"\n",
"This notebook demonstrates how to use the GHG Audit Export APIs to retrieve a full audit record of API activity for your organization for a given date range.\n",
"\n",
"The export is generated asynchronously. The workflow is:\n",
"\n",
"1. **Trigger** — submit an export request with a date range using `POST /audit`.\n",
"2. **Poll** — check the status of the request using `GET /audit/status` until the status is `COMPLETED` or `FAILED`.\n",
"3. **Download** — retrieve the ZIP archive using `GET /audit/download` once the status is `COMPLETED`.\n",
"\n",
"All endpoints require an admin bearer token. If the authenticated user is not an admin, the service returns `403 Forbidden`."
]
},
{
"cell_type": "markdown",
"id": "d4e5f6a7",
"metadata": {},
"source": [
"## Setup\n",
"\n",
"Ensure that Python 3+ is installed on your system.\n",
"\n",
"Note: To run this notebook, add your credentials to `../../../auth/secrets.ini` and `../../../auth/config.ini`.\n",
"\n",
"Example `secrets.ini` format:\n",
"\n",
"```\n",
"[EAPI]\n",
"api.pat_token = \n",
"api.tenant_id = \n",
"```"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "e5f6a7b8",
"metadata": {},
"outputs": [],
"source": [
"# Install the prerequisite Python packages\n",
"%pip install pandas configparser IPython requests"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "f6a7b8c9",
"metadata": {},
"outputs": [],
"source": [
"import pandas as pd\n",
"import configparser\n",
"import requests\n",
"import json\n",
"import time\n",
"from IPython.display import display as display_summary"
]
},
{
"cell_type": "markdown",
"id": "a7b8c9d0",
"metadata": {},
"source": [
"### Authentication Token\n",
"\n",
"Run the following code snippet to generate a Bearer Token using your PAT token. These admin endpoints require an authenticated admin user."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "b8c9d0e1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Authentication Success\n"
]
}
],
"source": [
"config = configparser.RawConfigParser()\n",
"config.read(['../../../auth/secrets.ini','../../../auth/config.ini'])\n",
"\n",
"EAPI_PAT_TOKEN = config.get('EAPI', 'api.pat_token')\n",
"EAPI_TENANT_ID = config.get('EAPI', 'api.tenant_id')\n",
"\n",
"EAPI_AUTH_ENDPOINT = config.get('EAPI', 'api.auth_endpoint')\n",
"EAPI_BASE_URL = config.get('EAPI', 'api.base_url')\n",
"\n",
"EAPI_AUTH_CLIENT_ID = 'saascore-' + EAPI_TENANT_ID\n",
"EAPI_CLIENT_ID = 'ghgemissions-' + EAPI_TENANT_ID\n",
"\n",
"auth_request_headers: dict = {}\n",
"auth_request_headers[\"X-IBM-Client-Id\"] = EAPI_AUTH_CLIENT_ID\n",
"auth_request_headers[\"X-IBM-Envizi-Pat\"] = EAPI_PAT_TOKEN\n",
"\n",
"verify = True\n",
"\n",
"auth_url = f\"{EAPI_AUTH_ENDPOINT}\"\n",
" \n",
"response = requests.post(url = auth_url,\n",
" headers = auth_request_headers,\n",
" verify = verify\n",
" )\n",
"if response.status_code == 200:\n",
" jwt_token = response.text\n",
" print(\"Authentication Success\")\n",
"else: \n",
" print(\"Authentication Failed\")\n",
" print(response.text)"
]
},
{
"cell_type": "markdown",
"id": "c9d0e1f2",
"metadata": {},
"source": [
"## Trigger Audit Export\n",
"\n",
"Use `POST /audit` to queue an audit export job for a given date range.\n",
"\n",
"The request body accepts the following parameters:\n",
"\n",
"- `fromDate` (required): Start date of the audit period (inclusive), in `YYYY-MM-DD` format.\n",
"- `toDate` (required): End date of the audit period (inclusive), in `YYYY-MM-DD` format.\n",
"- `apiName` (optional): Filter the export to a specific API endpoint — partial match on endpoint path.\n",
"\n",
"The endpoint returns:\n",
"\n",
"- `202 Accepted` for a fresh request, or a retry after a previous failure.\n",
"- `409 Conflict` if an identical request is already in progress or has already completed."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d0e1f2a3",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Status Code: 202\n",
"requestId: tenant-uuid/20250101/20250331\n"
]
}
],
"source": [
"AUDIT_ENDPOINT = f\"{EAPI_BASE_URL}/audit\"\n",
"\n",
"payload = {\n",
" 'fromDate': '2025-01-01',\n",
" 'toDate': '2025-03-31'\n",
" # Optional: uncomment to filter by a specific API endpoint\n",
" # 'apiName': '/location'\n",
"}\n",
"\n",
"request_headers: dict = {}\n",
"request_headers['Content-Type'] = 'application/json'\n",
"request_headers['x-ibm-client-id'] = EAPI_CLIENT_ID\n",
"request_headers['Authorization'] = 'Bearer ' + jwt_token\n",
"\n",
"response = requests.post(\n",
" AUDIT_ENDPOINT,\n",
" headers=request_headers,\n",
" data=json.dumps(payload)\n",
")\n",
"\n",
"print(f'Status Code: {response.status_code}')\n",
"\n",
"if response.status_code == 202:\n",
" response_json = response.json()\n",
" request_id = response_json.get('requestId')\n",
" print(f'requestId: {request_id}')\n",
" display_summary(pd.json_normalize(response_json))\n",
"elif response.status_code == 409:\n",
" print('Conflict: an identical request is already in progress or completed.')\n",
" print(response.text)\n",
"else:\n",
" print(response.text)"
]
},
{
"cell_type": "markdown",
"id": "e1f2a3b4",
"metadata": {},
"source": [
"### Output Description\n",
"\n",
"requestId - Unique identifier for the export request. Pass this to the status and download endpoints.\n",
"\n",
"status - Current processing status. On acceptance this will be `QUEUED`.\n",
"\n",
"message - Human-readable result message.\n",
"\n",
"submittedAt - ISO-8601 timestamp indicating when the request was submitted.\n",
"\n",
"links - Hypermedia links for the status check and download endpoints.\n",
"\n",
"Possible HTTP status codes:\n",
"\n",
"- `202` - Request accepted — fresh request or retry after a previous failure\n",
"- `400` - Bad request — missing or invalid parameters\n",
"- `403` - Forbidden — admin token required\n",
"- `409` - Conflict — request is already in progress or already completed\n",
"- `500` - Internal server error"
]
},
{
"cell_type": "markdown",
"id": "f2a3b4c5",
"metadata": {},
"source": [
"## Get Audit Export Status\n",
"\n",
"Use `GET /audit/status` to check the current processing status of an audit export request.\n",
"\n",
"Pass the `requestId` returned by `POST /audit` as a query parameter. The endpoint always returns `HTTP 200` for a valid `requestId`; the `status` field in the response body indicates the actual job outcome.\n",
"\n",
"The tenant extracted from the token must match the tenant embedded in the `requestId`.\n",
"\n",
"Possible `status` values:\n",
"\n",
"| Status | Description |\n",
"|---|---|\n",
"| `QUEUED` | Request accepted and waiting to be processed |\n",
"| `IN_PROGRESS` | Report is currently being generated |\n",
"| `COMPLETED` | Report is ready for download |\n",
"| `FAILED` | Report generation failed — see `message` for details |"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a3b4c5d6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Polling status...\n",
"Status: QUEUED\n",
"Status: IN_PROGRESS\n",
"Status: COMPLETED\n",
"Export is ready for download.\n"
]
}
],
"source": [
"AUDIT_STATUS_ENDPOINT = f\"{EAPI_BASE_URL}/audit/status\"\n",
"\n",
"request_headers: dict = {}\n",
"request_headers['Content-Type'] = 'application/json'\n",
"request_headers['x-ibm-client-id'] = EAPI_CLIENT_ID\n",
"request_headers['Authorization'] = 'Bearer ' + jwt_token\n",
"\n",
"print('Polling status...')\n",
"\n",
"while True:\n",
" response = requests.get(\n",
" AUDIT_STATUS_ENDPOINT,\n",
" headers=request_headers,\n",
" params={'requestId': request_id}\n",
" )\n",
"\n",
" if response.status_code != 200:\n",
" print(f'Unexpected status code: {response.status_code}')\n",
" print(response.text)\n",
" break\n",
"\n",
" status_json = response.json()\n",
" current_status = status_json.get('status')\n",
" print(f'Status: {current_status}')\n",
"\n",
" if current_status == 'COMPLETED':\n",
" print('Export is ready for download.')\n",
" display_summary(pd.json_normalize(status_json))\n",
" break\n",
" elif current_status == 'FAILED':\n",
" print(f'Export failed: {status_json.get(\"message\")}')\n",
" break\n",
" else:\n",
" # QUEUED or IN_PROGRESS — wait before polling again\n",
" time.sleep(10)"
]
},
{
"cell_type": "markdown",
"id": "b4c5d6e7",
"metadata": {},
"source": [
"### Output Description\n",
"\n",
"requestId - The unique identifier of the export request.\n",
"\n",
"status - Current job status: `QUEUED`, `IN_PROGRESS`, `COMPLETED`, or `FAILED`.\n",
"\n",
"submittedAt - ISO-8601 timestamp when the request was submitted. Present for `QUEUED`, `IN_PROGRESS`, and `FAILED` states.\n",
"\n",
"completedAt - ISO-8601 timestamp when the export completed. Present only when `status` is `COMPLETED`.\n",
"\n",
"message - Human-readable detail. Present when `status` is `FAILED` — describes why the export could not be generated.\n",
"\n",
"links.download - Download URL. Present only when `status` is `COMPLETED`.\n",
"\n",
"Possible HTTP status codes:\n",
"\n",
"- `200` - Status returned — always HTTP 200 for a valid `requestId`\n",
"- `400` - Invalid or missing `requestId`, or `requestId` belongs to a different tenant\n",
"- `403` - Forbidden — admin token required"
]
},
{
"cell_type": "markdown",
"id": "c5d6e7f8",
"metadata": {},
"source": [
"## Download Audit Export\n",
"\n",
"Use `GET /audit/download` to download the completed audit export. This endpoint is only available when the export status is `COMPLETED`.\n",
"\n",
"The response is a ZIP archive (`application/octet-stream`) containing a single CSV file named `audit-export.csv`.\n",
"\n",
"The tenant extracted from the token must match the tenant embedded in the `requestId`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d6e7f8a9",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Status Code: 200\n",
"Export saved to: audit-export.zip\n"
]
}
],
"source": [
"AUDIT_DOWNLOAD_ENDPOINT = f\"{EAPI_BASE_URL}/audit/download\"\n",
"\n",
"request_headers: dict = {}\n",
"request_headers['x-ibm-client-id'] = EAPI_CLIENT_ID\n",
"request_headers['Authorization'] = 'Bearer ' + jwt_token\n",
"\n",
"response = requests.get(\n",
" AUDIT_DOWNLOAD_ENDPOINT,\n",
" headers=request_headers,\n",
" params={'requestId': request_id}\n",
")\n",
"\n",
"print(f'Status Code: {response.status_code}')\n",
"\n",
"if response.status_code == 200:\n",
" output_file = 'audit-export.zip'\n",
" with open(output_file, 'wb') as f:\n",
" f.write(response.content)\n",
" print(f'Export saved to: {output_file}')\n",
"elif response.status_code == 409:\n",
" print('Report is still processing or generation failed. Check status before downloading.')\n",
"else:\n",
" print(response.text)"
]
},
{
"cell_type": "markdown",
"id": "e7f8a9b0",
"metadata": {},
"source": [
"The downloaded ZIP archive contains a single file: `audit-export.csv`.\n",
"\n",
"Possible HTTP status codes:\n",
"\n",
"- `200` - ZIP file returned successfully\n",
"- `400` - Invalid `requestId` for this tenant\n",
"- `403` - Forbidden — admin token required\n",
"- `409` - Report still processing, or generation failed"
]
},
{
"cell_type": "markdown",
"id": "f8a9b0c1",
"metadata": {},
"source": [
"## Related Links\n",
"\n",
"[Emissions API Developer Guide](https://developer.ibm.com/apis/catalog/ghgemissions--ibm-envizi-emissions-api/Introduction)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": ".venv",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbformat_minor": 5,
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.12.10"
}
},
"nbformat": 4,
"nbformat_minor": 5
}