Audit streaming
A stream sends a workspace’s whole audit log to where your security team keeps the rest of theirs: every request the edge let through or turned away, and every change anyone made. It goes in batches, a minute or so behind live. A stream can send to:
- Splunk, through an HTTP Event Collector, on Splunk Cloud or your own.
- Datadog, through its log intake, on your Datadog site.
- HTTPS, a URL of yours that takes each batch as signed JSON. Use it for Microsoft Sentinel, Elastic, Sumo Logic, Cribl or a collector of your own.
- An S3 bucket: Cloudflare R2, AWS S3, or another that speaks S3. Many SIEMs, Sentinel and Splunk among them, read a bucket.
If you’d rather fetch the log than have it sent, read it from the audit API with a cursor.
Streaming is an Enterprise feature. It’s included on Business for early customers, for a limited time, as the audit log’s CSV is. On Free and Team, adding a stream is refused with 403 plan_limit. A workspace can have 1 stream on Business and 3 on Enterprise. Adding one past that is refused with 403 and the code contact_sales. The message names the plan that allows more, or says no plan does, and then the only way to add one is to remove some first. A workspace with more, from when its plan allowed them, keeps them all sending, but can’t add another until it’s under its limit.
A workspace’s owners add, pause and remove streams. Its admins can see them, as they can read the log. Every owner gets an email when a stream is added or turned back on, saying who did it and where it sends, and the change is in the audit log, so the workspace’s webhooks and Slack channels hear of it too.
Add a stream
Section titled “Add a stream”In the dashboard, open the workspace’s Audit streaming page and select Add stream. Choose where it sends, and fill in where and how to sign in there. If you signed in more than 15 minutes ago, we ask you to sign in again first.
From the CLI, the credential is asked for at the prompt, or read from stdin, so it stays out of your shell history:
jiayang audit-stream add acme --splunk https://http-inputs-acme.splunkcloud.com --index securityjiayang audit-stream add acme --datadog datadoghq.eu --tags env:prodjiayang audit-stream add acme --http https://collector.example.com/jiayang --header Authorizationjiayang audit-stream add acme --s3 acme-audit --region us-east-1 --access-key-id AKIAIOSFODNN7EXAMPLE --prefix jiayang/For R2, add --endpoint https://<account id>.r2.cloudflarestorage.com and use --region auto. A stream starts with what happens from the moment it’s added. Send a test entry to check it:
jiayang audit-stream test acme <id>We keep every credential encrypted, and never show it again, to you or in the log. The dashboard and the log show only the kind of stream and where it sends, as far as that can be shown: the domain of a URL, the Datadog site, the bucket and its prefix. To change a credential, remove the stream and add it again.
Splunk
Section titled “Splunk”Give the collector’s address and an HEC token. The address can be the host and port alone, like https://http-inputs-acme.splunkcloud.com or https://splunk.example.com:8088, and we send to /services/collector/event on it. It can be on port 443, 8443 or 8088. Give an index to send to, or leave it out for the token’s default.
Each entry is one event, with sourcetype jiayang:audit, source jiayang, host jiayang.cloud, its time as the event’s time, and the entry itself as the event.
Datadog
Section titled “Datadog”Give your Datadog site, the one in your Datadog address: datadoghq.com, us3.datadoghq.com, us5.datadoghq.com, datadoghq.eu, ap1.datadoghq.com, ap2.datadoghq.com or ddog-gov.com. And an API key, from Organization Settings, API Keys. An application key won’t work.
Each entry is a log with ddsource jiayang, service jiayang-cloud, the tag workspace:<slug> and any tags you gave, a message that says what it is, like access deny (no_grant) on payroll, and the entry itself under audit. Its time is in date.
Give a URL that takes JSON. It has to start with https://, name a public host, and use port 443 or 8443, as a webhook’s does. If your collector wants a token, give a header to send with each batch, like Authorization, and its value.
Each batch is a POST of a JSON array of entries, with these headers:
| Header | What it holds |
|---|---|
Content-Type |
application/json |
Jiayang-Signature |
t=<unix time>,v1=<signature>, as a webhook’s |
Jiayang-Batch |
The batch’s id. The same on every try |
Jiayang-Stream |
The stream’s id |
We show the signing secret once, when the stream is made. It starts with jyw_. Check the signature as you would a webhook’s.
S3, R2 and other buckets
Section titled “S3, R2 and other buckets”Give the bucket, its region, and an access key ID and secret access key that can write to it. For AWS, leave the endpoint out. For R2, the endpoint is https://<account id>.r2.cloudflarestorage.com and the region is auto. For another store that speaks S3, give its endpoint. The key needs nothing but writing objects to the bucket, so make one that can do only that.
Each batch is one object: a gzip of one entry per line, as JSON. It’s named by the day and time the batch was made, under the prefix if you gave one:
jiayang/2026/10/04/20261004T140531Z_8a3c7c0e-0d4f-4e3a-9c2b-5b1f0e6d2a11.json.gzA test entry goes in an object whose name has test_ before the id.
What each entry holds
Section titled “What each entry holds”An entry is what the audit API returns for it, the same fields as jiayang --json audit, with the workspace it’s from. The actor’s session id, which is ours, is left out, as a webhook leaves it out:
{ "id": "0b8e6c1e-3d4f-4c51-9d7a-2a41f3f0c9e2", "at": "2026-10-04T14:05:31.204Z", "source": "edge", "action": "access", "decision": "deny", "reason": "no_grant", "status": 404, "app_id": "9f2a…", "app": "payroll", "app_deleted": false, "entry": "browser", "actor": { "kind": "user", "email": "ana@example.com" }, "host": "payroll--acme.jiayang-apps.cloud", "method": "GET", "path": "/", "detail": null, "ingested_at": "2026-10-04T14:05:31.611Z", "workspace": { "id": "5b1c…", "slug": "acme" }}The audit log says what each action means. A test entry has the action ping.
One kind of entry isn’t sent. With your own encryption key, a stream calls your key to send, and that call is a workspace_key.use entry for audit_stream. No stream sends those on, since each would call your key again. They’re in the log, and every other call to your key is sent.
Delivery, retries and failed batches
Section titled “Delivery, retries and failed batches”A stream reads the log a little behind live, so an entry that took a moment to reach the log isn’t skipped. Each entry is sent once. If we lose track of a batch partway, we send it again with the same Jiayang-Batch id, or to the same object name, so skip a batch or entry id you already have. A batch holds up to 500 entries, and stays under a megabyte.
A batch has 30 seconds to be answered with any 2xx. A batch that fails is tried again after a minute, then 2, 4, 8 minutes and so on, up to an hour apart, for a day. If it still fails after a day, we keep it as a failed batch, turn the stream off, and email the workspace’s owners. A stream that’s off sends nothing, and what happens meanwhile waits in the log.
Failed batches are kept for a week. To send them again, once the destination works:
jiayang audit-stream failed acme <id>jiayang audit-stream retry acme <id>jiayang audit-stream resume acme <id>retry sends every failed batch again, or one with --batch <id>. resume turns the stream back on, and it picks up where it stopped, as far back as a week. The dashboard does the same with Send failed batches again and Turn on, and Recent batches lists what was sent, with how each went.
We also turn a stream off if the workspace’s plan no longer includes streaming, or if whoever added it, or last turned it on, is no longer an owner or admin of the workspace. Each time we turn a stream off, the log has an audit_stream.off entry saying why. Batches it was still trying to send wait, as they do when you pause it, and go when it’s turned back on, with a fresh day before any of them counts as failed. A stream off for more than a week, paused or turned off by us, drops the batches that waited, since turned back on it reads the log back only a week anyway. Turned back on by another owner, it depends on them from then on, and the list still says who added it.
After a restore, a stream whose maker is held until your identity provider confirms them stays on, but sends nothing, a test included. The Audit streams page and jiayang audit-stream list say it’s waiting. Once your provider confirms them, or an owner adds them again, it sends what waited.
jiayang audit-stream test sends one ping entry now and says how it went. It exits 1 when it wasn’t delivered, so a script can tell. A test isn’t tried again, and isn’t one of the stream’s failed batches: failed doesn’t list it and retry doesn’t send it. On a plan without streaming, a test is refused, as adding a stream is.
Where we send
Section titled “Where we send”A stream never sends to a private, loopback or link-local address, however the address is written, and we check the host’s addresses again every time we send. It can’t point at Jiayang Cloud itself, and it doesn’t follow a redirect. These are the same rules as for webhooks.
Read it from the API instead
Section titled “Read it from the API instead”The audit API returns the log a page at a time, with a cursor to follow it. Call it with a CLI session’s token, from jiayang login, as the bearer token. A session lasts 30 days, so a puller that runs unattended needs its token renewed, or use a stream instead.
curl -H "Authorization: Bearer $JIAYANG_TOKEN" \ "https://api.jiayang.cloud/v1/workspaces/acme/audit?after=2026-10-04T00:00:00Z&settled=true&limit=500"| Parameter | What it does |
|---|---|
after |
Entries that reached the log after this RFC 3339 time, oldest first. |
after_id |
With after, the id of the last entry you have, so entries that reached the log at the same moment aren’t skipped. |
settled |
With after, true stops the page two minutes short of now. A change is in the log as of when it began, and can take up to a minute to finish, so past two minutes nothing older can still arrive. Follow with it and you never miss an entry. |
limit |
1 to 500 entries. The default is 100. |
app |
Only this app’s entries. |
decision |
deny for refused requests only. |
To follow the log, save the ingested_at and id of the last entry on each page and pass them as after and after_id next time. A page shorter than limit means you’ve caught up for now. Owners and admins read the whole log. An app’s owner reads that app’s entries.
Next steps
Section titled “Next steps”- Audit log: what each entry and action means, and how long it’s kept.
- Workspace webhooks: the workspace’s changes, one event at a time.