Export the sign-in log
Download your app’s sign-in log as a CSV from the portal, or pull it into your own SIEM or data store with the SDK — in all six languages.
What the log holds
One row per sign-in event for an app: push approvals, one-time codes, machine (SSH and desktop) sign-ins, and gaps where a machine could not report. The CSV header and the SDK keys use the same fields, in this order.
| Field | Meaning |
|---|---|
| id | Unique id of the row. |
| occurred_at | When the sign-in happened, RFC 3339 UTC. For an offline machine this is the machine’s own time. |
| received_at | When we recorded the row, RFC 3339 UTC. |
| user_identifier | Your identifier for the user, or the machine’s local login when there is none. |
| local_login | The local account name on the machine, when one applies. |
| kind | push, totp, machine or gap. See below. |
| origin | push (a push approval), machine_report (a registered machine reported it) or gap (a gap marker). Empty on one-time-code rows and on rows recorded before machine reporting existed. |
| method | Machine rows only, empty otherwise: totp, backup_code, unknown (a code was tried but its type could not be told) or none (nothing was proved, because the machine let the sign-in through without a factor or had none to ask for). |
| outcome | Push rows: approved, denied or expired. One-time-code rows from the SDK: verified. Machine rows: allowed or denied. Gap rows: gap. |
| reason | Machine rows: wrong_code, replayed_code, throttled, store_error or no_offline_factor on a denial; failopen, bypass_user, unenrolled_bypass or disabled on an allow with no factor. Gap rows: dropped (the machine discarded events) or missing (events never arrived). Empty otherwise. |
| city, region, country | Coarse location. Raw coordinates are never exposed. |
| new_country | True when the country is new for that user. |
| source_ip | The address the sign-in came from, when known. |
| machine_id, machine_name | The machine, for machine rows. |
| service | The service signed in to, such as sshd or sudo. |
| context | The context text shown to the user. |
| approval_id | Links the rows that describe the same push approval. |
| seq | Machine rows only: the machine’s own sign-in counter, used to detect gaps. Empty on push and one-time-code rows. |
| gap_count | On a gap row, how many sign-ins are missing. |
| clock_skew_seconds | How far the machine clock was from ours, when reported. |
The four kinds
- push — a tap-to-approve request.
- totp — a one-time code check.
- machine — a sign-in on a registered Linux or Windows machine, reported by the machine.
- gap — a marker that a machine was unable to report some sign-ins. gap_count says how many.
Download a CSV from the portal
Open an app and choose its Activity tab, set the filters you want and press Download CSV. The file holds exactly what those filters select, newest first, and matches the log on screen — an expired push is hidden when its machine report exists. The file is UTF-8 with a byte-order mark so Excel opens accented names correctly. Cells that start with =, +, - or @ are prefixed so a spreadsheet does not run them as formulas.
Pull it with the SDK
Load your saved cursor, request a page, store the rows, save the new cursor, and repeat while has_more is true. Run it on a schedule. Pages hold up to 1000 rows; leave the limit out to take the default of 500.
cursor := loadCursor() // "" on the first run
for {
page, err := client.ListSignInLog(1000, cursor)
if err != nil {
return err
}
for _, ev := range page.Items {
store(ev)
}
cursor = page.NextCursor
saveCursor(cursor)
if !page.HasMore {
break
}
}Always save next_cursor, even when the page is empty: it is the position to resume from. A cursor that is not one we issued is refused with INVALID_CURSOR.
Oldest first, about 30 seconds behind
The feed is ordered oldest first, by the time we received each row. Rows are served about 30 seconds after they are recorded, so a fresh sign-in may not appear on the next pull. The portal CSV is newest first and has no such lag.
One expired push can be two rows
When a push expires and the machine later reports the same sign-in, the feed carries both rows, linked by the same approval_id. The portal screen and CSV show only the machine report; the SDK feed keeps both so you can reconcile them yourself.
Machine keys are refused
A key created for one registered machine cannot read the log: the call answers 403 MACHINE_KEY_NOT_ALLOWED. Create an ordinary app key in the portal and use that.
How far back it goes
Sign-ins are kept 7 days on Developer, 30 on Growth, 90 on Team and 365 on Scale. Pull on a schedule that is shorter than your plan’s window, and keep what you pull.