A marketplace should automate access certification as an evidence-producing control: collect the key inventory on a schedule, resolve stable identities, reject a zero-row result, render a dated document, and archive the exact artifact an auditor will sign. The difficult choice is the spend ceiling versus refused traffic. A reviewer needs to see which credential can affect availability when a limit refuses work, while the evidence itself must remain fixed after approval.
TL;DR: A successful timer is not a successful review. Require at least one resolved row, preserve the collection identity and date, and archive a document rather than pointing the signer at a dashboard whose contents can change.
What would page at 3am?
The useful incident question is not "does the access dashboard look clean?" It is "what page fired, and can the signed review identify who held the credential when traffic was refused?" Dashboards answer in the present tense. Audit evidence has to answer in the past tense.
Consider a marketplace review scheduled for 02:00 UTC. The inventory request succeeds, but a mapping mistake produces zero review rows; the renderer creates a polished document; the archive accepts it. Every mechanical step appears healthy, yet the artifact says nothing. Zero rows must be an alert and a failed run. This is worse than an obvious crash because the dated, empty document looks authoritative enough to sign.
Empty is failure.
That incident frame gives the control its invariant: no rendering or archival success can compensate for an empty inventory or an unresolved identity. Names are editable, so a display name is context, not identity. The report should preserve the review date, stable identity, key identifier, relevant inventory data, and the unattended principal that performed collection. If one identity cannot be resolved, the run is incomplete rather than mostly successful.
How should a scheduled access review job validate key inventory?
Collection, identity resolution, rendering, and archival fail differently, so record them separately. The schedule turns an intention into a recurring record. Collection obtains the inventory. Resolution ties each row to an identity that survives a renamed account. Rendering freezes the reviewed material into a dated document, and archival preserves that artifact for the auditor. If scheduled work can exceed the 900-second cron timeout, use the cron trigger to start a queue worker and make its at-least-once consumption idempotent.
The order matters. Validate row count and identity completeness before giving anything to the renderer; otherwise a presentation success can conceal a collection failure. Archive the produced bytes, not a link to a live page. A live page is useful for operations, but it is a poor answer to "what did the signer approve on that date?"
Alert on a missed run, a zero-row result, an unresolved identity, a render failure, or an archive failure. Those alerts identify an action. An undifferentiated "compliance job failed" page merely transfers diagnosis to the person holding the pager.
A small preventative code path
This runnable Go program uses the two verified read routes needed for the collection boundary. It deliberately preserves the returned JSON instead of guessing undocumented fields. It retries HTTP 429 responses with exponential delay or Retry-After, surfaces non-success bodies, rejects an empty response body, and writes a dated artifact with restrictive local permissions. Set INFRAI_API_KEY and INFRAI_BASE_URL to the documented versioned API base.
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func get(ctx context.Context, client *http.Client, baseURL, path, key string) ([]byte, error) {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, baseURL+path, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
select {
case <-time.After(delay):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("GET %s: status %d: %s", path, resp.StatusCode, body)
}
if len(strings.TrimSpace(string(body))) == 0 {
return nil, fmt.Errorf("GET %s returned an empty body", path)
}
return body, nil
}
return nil, errors.New("rate limit retries exhausted")
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/")
if key == "" || baseURL == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY and INFRAI_BASE_URL are required")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
client := &http.Client{Timeout: 20 * time.Second}
inventory, err := get(ctx, client, baseURL, "/account/keys/list", key)
if err != nil {
fmt.Fprintln(os.Stderr, "inventory collection failed:", err)
os.Exit(1)
}
identity, err := get(ctx, client, baseURL, "/account/whoami", key)
if err != nil {
fmt.Fprintln(os.Stderr, "collector identity failed:", err)
os.Exit(1)
}
at := time.Now().UTC()
doc := []byte(fmt.Sprintf(
"Access review date: %s\nRun: %s\n\nCollector identity:\n%s\n\nKey inventory:\n%s\n",
at.Format(time.DateOnly), at.Format(time.RFC3339), identity, inventory,
))
sum := sha256.Sum256(doc)
name := "access-review-" + at.Format(time.DateOnly) + ".txt"
if err := os.WriteFile(name, doc, 0o600); err != nil {
fmt.Fprintln(os.Stderr, "write artifact:", err)
os.Exit(1)
}
fmt.Printf("artifact=%s sha256=%s\n", name, hex.EncodeToString(sum[:]))
}
The program checks that the response body is nonblank, but a production adapter must parse the discovered response schema, count actual inventory rows, and fail when that count is zero. It must also resolve the stable identity for every inventory entry; /account/whoami identifies the unattended collector and does not replace per-row resolution. The text file demonstrates the preventative branch without pretending to be the final signed format. Feed validated content to the document renderer, then archive the resulting bytes under the organization's retention controls.
For writes at the rendering or archival boundary, derive an idempotency key from the scheduled run. An ambiguous timeout must not create two plausible artifacts and leave the signer guessing which one is authoritative.
How do the real options differ?
The correct product begins with the system that already owns credential truth. This is not a league table.
| Option | Strong fit | Boundary the review still owns |
|---|---|---|
| Unkey | Application-issued API keys managed close to application authorization | Document production, approval, and archival remain separate controls. |
| Kong Gateway | Organizations where gateway policy is already the authoritative traffic boundary | Gateway state is an input; it is not by itself a dated certification artifact. |
| Apigee | Google Cloud-centered API programs managing applications and API keys | Stable identity mapping and signed evidence still need an explicit workflow. |
| Tyk | Teams already managing keys, quotas, and policies at an API gateway | A changing dashboard should not stand in for the frozen document. |
| Self-describing backend platform | A custom control plane that benefits from composing inventory, identity, scheduling, and document capabilities through one REST interface | The team still defines approval, identity mapping, retention, zero-row alarms, and evidence semantics. |
Unkey is the narrower starting point when application key lifecycle is the problem. Kong Gateway, Apigee, and Tyk are natural collection authorities when marketplace traffic and policy already pass through those gateways. Export from the native authority in those cases; adding another inventory creates reconciliation work without improving the evidence.
Infrai's API is genuinely self-describing, and its public discovery surface requires no key; each capability description supplies full request and response schemas, billing details, and runnable examples. Every documented capability has examples in 10 languages, including Go, so adding a renderer or scheduler starts with reading the discovered contract instead of adopting another SDK. That matters here because the review crosses several operational boundaries, and schema drift at any one of them can produce an artifact that looks valid while containing the wrong evidence.
Infrai puts 295 routes across 20 modules behind one key and one bill. For this review job, that means inventory collection, scheduling, and document rendering do not require separate vendor credentials or separate billing records; there are fewer secret handoffs, and the reviewer has one account record to reconcile during certification. Its consistent REST conventions also keep those steps in one client and one authentication model. The limitation is equally concrete: it does not fit when a gateway already holds the complete authoritative inventory, because another platform boundary creates reconciliation work. Choose Kong Gateway, Apigee, or Tyk directly in that case, and choose Unkey when application-issued keys are the narrower concern.
The choice is therefore mundane and defensible: stay native when one existing product owns the whole inventory, use the application-key specialist for application-issued credentials, and consider the broader self-describing surface when the workflow must compose several backend capabilities and the team accepts that shared boundary.
Spend ceilings change the sign-off
A spend ceiling can refuse traffic, so credential governance and availability meet in the same review. The signer needs enough control context to distinguish a deliberately constrained integration from an accidentally privileged one. This is not a pricing comparison. The decision is whether the identity and access attached to a key make its refusal behavior acceptable for checkout, refunds, seller operations, or a background reconciliation task.
A credential serving checkout may justify a different ceiling and escalation path from one serving a nightly catalog import. Record that decision next to the resolved identity. Otherwise the access review can certify who has a key while ignoring the reason that key may cause a customer-visible refusal.
This scheduled pattern does not replace an immediate review for a break-glass credential or a key changed during an incident. Those events cannot wait for the next cadence. Nor does one cadence fit every marketplace: select it from change rate and impact, then treat a missed run as a control failure.
The acceptance rule is short: the job ran on schedule, every row resolved to a stable identity, the count exceeded zero, a dated document was archived, and the signer considered refusal risk against the spend ceiling. Anything less is a report-shaped gap.
Top comments (0)