Keeps the per-entity TriNet → Okta API tokens alive.
Okta deactivates an API token after 30 days of inactivity. Each TriNet entity’s HR-to-Okta integration uses its own SSWS token; entities that go a while without hiring never exercise their token, so it silently dies and the next new hire fails to provision. This script pings every token weekly with a cheap read-only call, which resets the 30-day idle clock.
It does double duty:
⚠️ Pinging cannot revive a dead token, and Okta only shows a token’s value once at creation. A token that’s already expired (or whose value you never saved) has to be regenerated in Okta and re-entered into TriNet by whoever owns the integration config. This tool tells you which ones; it can’t recover them.
GitHub Actions cron (~every 10 days: 1st/11th/21st, 08:00 UTC)
│
▼
ping.py
│ for each label -> token in OKTA_PING_TOKENS:
├──► GET https://<OKTA_DOMAIN>/api/v1/users?limit=1 (Authorization: SSWS <token>)
│ 200 → healthy (idle timer reset)
│ 401 → dead (regenerate + re-enter in TriNet)
│ else → errored (403 scope / 429 / 5xx / network)
│
├──► Slack incoming webhook (only when something needs attention,
│ or always if ALWAYS_NOTIFY=true)
│
└──► Confluence "API Key Status" table (optional — see below)
The run exits non-zero if any token is dead or errored, so the failed GitHub Actions run is itself a backstop signal even if Slack delivery is misconfigured.
All config is via environment variables (or a local .env — see
.env.example):
| Var | Required | Example |
|---|---|---|
OKTA_DOMAIN |
yes | schmidtentities.okta.com |
OKTA_PING_TOKENS |
yes | {"TriNet - Hillspire":"00aBc...","TriNet - Schmidt Ocean":"00dEf..."} |
SLACK_WEBHOOK_URL |
no | https://hooks.slack.com/services/T.../B.../... |
ALWAYS_NOTIFY |
no | true to post a heartbeat even when all healthy |
CONFLUENCE_BASE_URL |
no¹ | https://hillspire.atlassian.net/wiki |
CONFLUENCE_EMAIL |
no¹ | Audit Bot account email |
CONFLUENCE_API_TOKEN |
no¹ | Audit Bot Atlassian API token |
CONFLUENCE_SPACE_KEY |
no¹ | IPO |
CONFLUENCE_KEYLOG_PAGE_ID |
no¹ | page ID of the “Okta API Key Log” index page |
OKTA_API_TOKEN |
no² | service-account SSWS token with System Log read |
¹ Required only to enable the Confluence API Key Log (below) — all five
together, or none. ² Required only by keylog.py.
OKTA_PING_TOKENS is a single JSON object mapping a human label to each
entity’s SSWS token, kept on one line. Label each entry clearly (e.g.
"TriNet - <Entity>") so a dead-token alert points you straight at the entity
whose integration needs fixing.
If the CONFLUENCE_* settings are present, the integration maintains a Confluence
log that mirrors the Okta Audit Bot’s 3.C.1 structure — a folder, an index
page, and a page per month:
3.C.2 Okta API Key Log (folder — create manually in the UI)
└─ Okta API Key Log (index page = CONFLUENCE_KEYLOG_PAGE_ID)
• API Key Status table ← ping.py rewrites every keepalive run
• Monthly Log table ← Month | Page | Created | Revoked
└─ 2026-06 Okta API Key Log (monthly pages, auto-created)
• lifecycle events ← keylog.py appends create/revoke
ping.py each
run: label, status (✅ Active / ❌ Dead / ⚠️ Error), last pinged, result. It’s a
live snapshot, so it lives on the index rather than per-month.keylog.py polls the System Log for
system.api_token.create / system.api_token.revoke and appends a row
(when, event, key name, actor) to that month’s page, updating the index counts.ping.py (~every 10 days) ─► rewrites the index "API Key Status" table
keylog.py (every 15 min) ─► appends create/revoke to the month page (near real-time)
The two share a GitHub Actions concurrency group so they never write at once;
update_page also retries once on a version conflict. State (last System Log
timestamp) lives in a content property on the index; events are deduped by
timestamp — same approach as okta_audit_log.
Reuse the Okta Audit Bot service account for CONFLUENCE_* and
OKTA_API_TOKEN (it already has System Log read). OKTA_API_TOKEN is the
read-only service token — not one of the TriNet tokens in OKTA_PING_TOKENS.
.../pages/<id>/); the script installs the template
and creates monthly pages under it.CONFLUENCE_BASE_URL, CONFLUENCE_EMAIL,
CONFLUENCE_API_TOKEN, CONFLUENCE_SPACE_KEY, CONFLUENCE_KEYLOG_PAGE_ID,
OKTA_API_TOKEN (all but the page ID reuse the Audit Bot’s values).For any token you don’t have, generate a fresh one (Okta Admin Console → Security → API → Tokens → Create Token), record its value, and have your HRIS contact enter it into that entity’s TriNet integration. The creating account needs the same privileges TriNet uses to provision users.
Rather than hand-writing the OKTA_PING_TOKENS JSON, keep a friendly
tokens.txt (gitignored — see tokens.example.txt) with one Label = token
per line, then let the helper assemble the secret value:
python integrations/okta-api-ping/build-tokens.py # prints the one-line JSON
python integrations/okta-api-ping/build-tokens.py --env # prints OKTA_PING_TOKENS=...
Start with the few tokens you have and add lines as you mint the rest — the keepalive script pings whatever ends up in the secret.
api.slack.com/apps → your app → Incoming Webhooks → add one for the channel
you want alerts in. Copy the https://hooks.slack.com/... URL.
Repo settings → Secrets and variables → Actions:
| Secret | Value |
|---|---|
OKTA_DOMAIN |
schmidtentities.okta.com |
OKTA_PING_TOKENS |
the one-line JSON object of label → token |
SLACK_WEBHOOK_URL |
webhook for ping.py dead-token alerts (omit to disable) |
KEYLOG_SLACK_WEBHOOK_URL |
webhook for keylog.py key create/revoke alerts (omit to disable) |
Both scripts read the same SLACK_WEBHOOK_URL environment variable, but each
workflow maps a different secret into it — okta_api_keylog.yml maps
KEYLOG_SLACK_WEBHOOK_URL. That keeps the two alert paths independent: turning
on key-log alerts can’t accidentally switch on dead-token alerts, and they can
point at different channels.
OKTA_DOMAIN is already a secret if you’ve set up the okta_audit_log
integration — the same value is reused here.
Actions → Okta API Ping → Run workflow. The first run is your audit: check the logs (and Slack) for any dead/errored tokens, fix those, then let the weekly schedule take over.
cp integrations/okta-api-ping/.env.example integrations/okta-api-ping/.env
# fill in OKTA_DOMAIN and OKTA_PING_TOKENS, then:
python -m pip install -r integrations/okta-api-ping/requirements.txt
python integrations/okta-api-ping/ping.py --no-slack
--no-slack logs the summary instead of posting it — useful when you just want
to see token health locally.
/users?limit=1? It’s the cheapest authenticated read every
provisioning token is guaranteed to have access to. A 200 confirms the token
is live and resets its idle timer in one call.401 means the token is invalid/expired (dead). A 403
means it authenticated but lacks scope — the idle timer still resets, but it’s
flagged as “errored” because that’s not the token shape we expect.OKTA_PING_TOKENS secret. No code
change needed.