# Datafile (/docs/api/datafile)

The datafile contains the complete configuration of an environment for one [space](/docs/terminology#space). It contains all flags and tests that are active and links the audiences required for those entries.

## Url [#url]

`https://improve.obelism.studio/config/:spaceId/:environment/:status?`

- Method: `GET`
- `spaceId` - Unique identifier of the space; `space_XXX`. Copy it from Implementation on the [Settings page](/settings). An organization id (`org_XXX`) works too, and loads the organization's first space: that is what the SDK sends.
- `environment` - environment to load; `'develop' | 'staging' | 'production'`
- `status` - _(optional)_ which test state to return; `'draft' | 'active' | 'finished' | 'archived'`. When omitted it defaults to `active`, so `/config/:spaceId/:environment` returns the same as `/config/:spaceId/:environment/active`.

The `status` segment lets you preview tests in another state before they go live: for example, request `draft` to validate a test you're still setting up. Production traffic should keep loading `active` (the default).

A space or organization id that doesn't exist, or belongs to an Organization that was deleted, gets `404` with the code [`unknown_organization`](/docs/api/errors#unknown_organization). The SDK doesn't retry it. Path parameters that aren't valid at all (an id without the `space_` or `org_` prefix, an unknown environment or status) still get an empty datafile, `{}`, so the SDK falls back to its defaults.

## Authentication [#authentication]

How a request is checked depends on whether it sends an `Origin` (see [Security](/docs/api)):

- **Browser (client) requests** send an `Origin`, which must be in the environment's **Allowed origins** (configured in the console; an entry ending in `:*`, like develop's default `http://localhost:*`, matches any port). This is enforced via CORS: an origin that isn't allow-listed receives a `403`.
- **Server requests** don't send an `Origin` and are served without a token, rate limited per IP (see below). Send the environment's API token in a `token` header to lift that limit (the edge limit still applies). A token that isn't valid isn't refused: the request is served and rate limited like one without a token.

Allowed origins decide which sites can load the datafile in a browser; they don't make it secret. Anyone who knows the Organization id can fetch it without an `Origin`, so don't put anything in a test or flag that visitors shouldn't see.

## Rate limiting [#rate-limiting]

The browser (Origin) path is rate-limited per IP (currently 600 requests/minute); exceeding it returns `429`. Browser responses carry `RateLimit-Policy: "browser";q=600;w=60`, and a `429` adds `RateLimit: "browser";r=0;t=60` and `Retry-After: 60` (see [Versioning](/docs/api/versioning#rate-limit-headers)). Requests with a valid `token` are exempt, so server-side rendering behind a shared egress IP is only held to the edge limit below. A `token` that isn't one of the environment's API tokens is rate limited like a request without one.

Every datafile request, with or without a token and cached or not, is also limited at Cloudflare's edge to 100 per 10 seconds per IP. Past it, the address gets a `429` `rate_limited` for 10 seconds. That response comes from the edge, not the API: it is `application/json` (the same problem fields) without `RateLimit` or `Retry-After` headers, so wait 10 seconds before retrying.

Responses are also served with a short CDN cache (30 seconds, then served stale for up to 60 more while it refreshes in the background, with `Vary: Origin`), which absorbs repeated identical requests before they reach the origin. Browsers don't cache it (`Cache-Control: public, max-age=0`). Saving a test, flag or audience clears the cached datafile, so the change usually shows up on the next request; in the rare case that fails, it takes up to about 90 seconds. Changes to API tokens and allowed origins take up to about two and a half minutes.

## Caching [#caching]

Loading the datafile is one of the metrics on Improve that could increase costs. If you'd run a test on the server and want to post analytics from the client, this could mean two requests to the datafile per visitor. To make sure you don't rapidly chew through your usage plan you can cache this file in a few ways;

### Add a caching layer [#add-a-caching-layer]

Instead of requesting the file from Improve for every visitor you could place it in your own key/value storage (like Redis). Here you can configure a time-to-live, ttl for short, and load it once every 5 minutes or per hour. This can be a great solution if you're not making changes on the fly. However, keep in mind that disabling all traffic from a flag back to the original will have the caching time as a delay.

### Statically generate [#statically-generate]

If you hardly make any changes in your config but you have one AB test running for a few weeks, or you're a solo developer that can easily deploy when you make a change, it could be an idea to grab the file on deploy and host it yourself in your project. A way of doing this is with pre-deploy scripts.
For example, if you host using GitHub Actions, you could add a task to grab it then. For hosting providers like Vercel and Netlify you utilize a _prebuild_ script to download the latest datafile at that point.

## Type [#type]

Audiences match against parameters derived from the visitor. Most (`pointer`, `device`, `browser`, `os`) come from the user agent and resolve anywhere. `country` (ISO 3166-1 alpha-2, e.g. `NL`) is the exception: the browser can't determine it from the user agent, so it only resolves during **server-side / middleware bucketing**: the [Next.js middleware](/docs/sdk/nextjs) supplies it automatically from the edge, or you pass it into the server SDK's [getTestValue](/docs/sdk/javascript#gettestvalue) / [getFlagValue](/docs/sdk/javascript#getflagvalue) `geo` argument. A `country` audience never matches client-side.

```ts
type AudienceParamKey = 'country' | 'pointer' | 'device' | 'browser' | 'os'

export type Configuration = {
	name: string
	version: number
	flags: {
		[flagSlug in string]: {
			id: string
			name: string
			audience: string
			options: {
				name: string
				slug: string
				value: string | undefined
				split: number
			}[]
		}
	}
	tests: {
		[testSlug in string]: {
			id: string
			name: string
			defaultValue: string
			audience: string
			allocation: number
			options: {
				name: string
				slug: string
				value: string | undefined
				split: number
			}[]
			events: Events
		}
	}
	audience: {
		[audienceSlug in string]: {
			[Key in AudienceParamKey]: string
		}
	}
}
```

## Example [#example]

```ts
{
  "name": "obelism",
  "version": 1,
  "flags": {
    "feature-test": {
      "id": "feat_VGEFKIJEXBLBRB5KUCZJSTNMD7",
      "name": "Feature test",
      "audience": "cooler-users",
      "options": [
        {
          "name": "Original",
          "slug": "original",
          "value": "a",
          "split": 80
        },
        {
          "name": "Alteration",
          "slug": "alteration",
          "value": "b",
          "split": 20
        }
      ]
    }
  },
  "tests": {
    "duo-test": {
      "id": "test_AM1TCFPYQW08EQFNWHL5ZSP8VB",
      "name": "Duo test",
      "defaultValue": "control",
      "allocation": 100,
      "audience": "",
      "options": [
        {
          "name": "Control",
          "slug": "control",
          "value": "a",
          "split": 50
        },
        {
          "name": "Variation",
          "slug": "variation",
          "value": "b",
          "split": 50
        }
      ],
      "events": {
        "start": "page_view",
        "metrics": [],
        "conversion": "sign_up"
      }
    }
  },
  "audience": {
    "cooler-users": {
      "device": "tablet",
      "os": "ios"
    },
    "dutch-visitors": {
      "country": "NL"
    }
  }
}
```
