API Documentation
Everything you need to generate realistic mock data with the Apiryner API.
Quick Start
Generate random data in 3 steps:
- Create a Layout — define your data schema with field names and types
- Get an API Key — create one from the API Keys page
- Make a Request — call the API with your layout slug
Example request:
curl -H "X-API-Key: ak_your_key_here" \
"https://api.apiryner.com/v1/layouts/user-profile?count=2&locale=en"
Example response:
[
{
"id": 1,
"name": "Sarah Johnson",
"email": "[email protected]",
"age": 28,
"is_active": true
},
{
"id": 2,
"name": "James Wilson",
"email": "[email protected]",
"age": 45,
"is_active": true
}
]
Authentication
Apiryner supports two authentication methods:
API Key (Read-Only)
Pass your API key in the X-API-Key header. API keys can only read/generate data.
curl -H "X-API-Key: ak_your_key_here" \
https://api.apiryner.com/v1/layouts/my-layout
Bearer Token (Full Access)
Use an OAuth 2.0 Bearer token for write access (create, update, delete). The easiest way is the CLI login, which runs the OAuth PKCE flow and stores the token locally:
# Login via OAuth PKCE (opens the browser)
npx apiryner login
# Use token
curl -H "Authorization: Bearer oat_your_token" \
-X POST https://api.apiryner.com/v1/layouts \
-H "Content-Type: application/json" \
-d '{"name": "Users", "slug": "users", "schema": {"id": {"type": "uuid"}}}'
Tokens carry the scopes granted on the consent screen: read allows GET requests, write allows everything. A request outside the token's scopes returns 403 insufficient_scope; write operations with an API key return 403 forbidden.
Endpoints
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| Generation | |||
| GET | /v1/layouts/:slug | API Key / Bearer | Generate data from a layout |
| GET | /v1/bundles/:slug | API Key / Bearer | Generate every layout in a bundle in one response |
| GET | /v1/snapshots/:slug | API Key / Bearer | Serve a snapshot’s pinned data |
| Layouts | |||
| GET | /v1/layouts | API Key / Bearer | List your layouts |
| POST | /v1/layouts | Bearer | Create a layout |
| PUT | /v1/layouts/:slug | Bearer | Update a layout |
| DELETE | /v1/layouts/:slug | Bearer | Delete a layout |
| POST | /v1/layouts/import | Bearer | Infer a schema from a JSON sample and save it as a layout |
| Bundles | |||
| GET | /v1/bundles | API Key / Bearer | List your bundles |
| POST | /v1/bundles | Bearer | Create a bundle from layout IDs |
| PUT | /v1/bundles/:slug | Bearer | Update a bundle |
| DELETE | /v1/bundles/:slug | Bearer | Delete a bundle |
| Snapshots | |||
| GET | /v1/snapshots | API Key / Bearer | List your snapshots |
| POST | /v1/snapshots | Bearer | Generate a dataset and pin it as a snapshot |
| PUT | /v1/snapshots/:slug | Bearer | Replace a snapshot’s name or data |
| DELETE | /v1/snapshots/:slug | Bearer | Delete a snapshot |
| Types & templates | |||
| GET | /v1/types | API Key / Bearer | The full data type catalog |
| GET | /v1/types/:name | API Key / Bearer | One type’s details |
| GET | /v1/templates | API Key / Bearer | List preset templates |
| GET | /v1/templates/:id | API Key / Bearer | One template’s layout definitions |
| POST | /v1/templates/:id/apply | Bearer | Create layouts from a template |
| Webhooks | |||
| GET | /v1/webhooks | API Key / Bearer | List your webhooks |
| POST | /v1/webhooks | Bearer | Subscribe a URL to events |
| DELETE | /v1/webhooks/:id | Bearer | Delete a webhook |
| GET | /v1/webhooks/:id/deliveries | API Key / Bearer | Recent delivery attempts |
| Account | |||
| GET | /v1/account/usage | API Key / Bearer | This period's request and record usage |
| GET | /v1/account/api-keys | API Key / Bearer | List your API keys |
| POST | /v1/account/api-keys | Bearer | Create an API key |
| PUT | /v1/account/api-keys/:id/deactivate | Bearer | Deactivate an API key |
| DELETE | /v1/account/api-keys/:id | Bearer | Delete an API key |
Every /v1 route needs credentials — there is no anonymous access. API keys are read-only, so rows marked Bearer need an OAuth token with the write scope. List endpoints accept limit (max 100) and offset.
Query Parameters for Data Generation
When calling GET /v1/layouts/:slug:
| Parameter | Type | Default | Description |
|---|---|---|---|
| count | integer | the layout’s own count | Number of records to generate. A value above your plan’s per-request maximum is capped, not rejected. |
| locale | string | the layout’s own locale | Data locale (en, ko, ja, zh). Use random to give each record a random locale — still reproducible with seed. |
| seed | integer | random | Seed for reproducible data. The same seed gives the same output; 0 counts as no seed. |
| format | string | json | Response format (json, csv) |
A layout saved as a single object — or any request with count=1 — answers with one JSON object instead of an array. GET /v1/bundles/:slug takes seed and locale only; GET /v1/snapshots/:slug takes format only, since its data is already fixed.
Data Types
Apiryner ships 30 built-in data types, organized by category. Use the type field in your schema to pick each field’s generator; GET /v1/types returns the same catalog at runtime.
Basic
| Type | Description | Options |
|---|---|---|
| integer | Random integer within a range | min, max |
| decimal | Random decimal number | min, max, precision |
| boolean | Random true/false value | true_rate |
| string | Random string of characters | length, charset |
| enum | Pick from a list of values | values |
Nested
| Type | Description | Options |
|---|---|---|
| object | Nested JSON object | properties (nested schema) |
| array | Array of items | items, min_items, max_items |
Personal
| Type | Description | Options |
|---|---|---|
| name | Full person name (family name first for ko/ja/zh) |
— |
Email address, built from the record’s own name when it has one |
— | |
| phone | Phone number | format |
| address | Full address | — |
| city | City name | — |
| zipcode | Postal/ZIP code | — |
| street | Street name with number | — |
Text
| Type | Description | Options |
|---|---|---|
| product_name | Product name | — |
| word | Random word | — |
| sentence | Random sentence | — |
| paragraph | Random paragraph | sentences |
| description | Descriptive text | length (short/medium/long) |
Date & Time
| Type | Description | Options |
|---|---|---|
| date | Random date | from, to, format |
| datetime | Random date and time, always RFC 3339 in UTC | from, to |
| time | Random time | format |
Finance
| Type | Description | Options |
|---|---|---|
| currency | Random monetary amount | currency, min, max |
| price | Product price (same generator as currency) |
currency, min, max |
Identifiers
| Type | Description | Options |
|---|---|---|
| uuid | UUID v4 | — |
| slug | URL-friendly slug | words |
| sequence | Auto-incrementing number | start, prefix |
Media
| Type | Description | Options |
|---|---|---|
| image_url | Random image URL (placeholder) | width, height |
| color | Random color code | format (hex/rgb/rgba) |
Relation
| Type | Description | Options |
|---|---|---|
| reference | Pick a value from another layout's generated records | layout, field, fallback |
Relations & Conditions
Reference fields
A reference field pulls its value from another layout's generated records, so related layouts share consistent keys. Point it at a target layout (by slug) and one of that layout's fields:
{
"id": { "type": "uuid" },
"user_id": {
"type": "reference",
"options": { "layout": "users", "field": "id" }
}
}
- In a bundle — layouts are generated in dependency order, and reference fields pick from the records in the same bundle response. Include every referenced layout in the bundle.
- Individual layout calls — HTTP requests are stateless, so a reference cannot reuse the records some earlier call returned. Each referenced layout is regenerated with your request's seed, its own array count, and its own locale. Two calls with the same seed therefore see identical parent records; a call with no seed sees fresh ones. To pin one parent dataset across several layouts, generate them together with a bundle call.
- Validation — the target layout must exist on your account and must actually have the field you name. Saving a layout that breaks either rule is rejected, in the API and in the web editor alike.
- Cycles — layouts that reference each other (directly or transitively) are rejected when saving and fail generation.
fallbackis returned when the referenced layout has no generated records.
Conditional fields
Any field can carry a condition that decides per record whether the field is included, based on a previously generated field in the same record:
{
"status": { "type": "enum", "options": { "values": ["active", "banned"] } },
"banned_reason": {
"type": "sentence",
"condition": { "field": "status", "equals": "banned" }
}
}
| Operator | Meaning | Value |
|---|---|---|
| equals / not_equals | Exact match against the referenced field | any value |
| in / not_in | Membership in a list | array of values |
| gt / gte / lt / lte | Numeric comparison | number |
When the condition doesn't match, the field is omitted from that record. The condition's field must name a field in the same object.
Bundles & Snapshots
Bundles
A bundle names a set of your layouts and generates them together in one response, so reference fields resolve against the very records you receive.
curl -H "X-API-Key: ak_your_key_here" \
"https://api.apiryner.com/v1/bundles/shop?seed=42"
The response is an object keyed by layout slug:
{
"users": [
{ "id": "8f0c1d2e-4b77-4c2a-9c31-2a7d0f6b1e55", "name": "Sarah Johnson" }
],
"orders": [
{ "id": "b1a70c93-2f18-49d6-8f0a-6c5e2b9d4471",
"user_id": "8f0c1d2e-4b77-4c2a-9c31-2a7d0f6b1e55", "total": 41.5 }
]
}
- Layouts are generated in dependency order, so a reference always sees its target’s records.
- A layout that references one outside the bundle gets an
errorunder its own key; the rest of the bundle still generates. - Record counts come from the bundle’s
config, falling back to each layout’s own count, and are capped by your plan. seedandlocaleapply to the whole bundle. There is nocountparameter and no CSV output.
Create one from the layout IDs it should hold:
curl -X POST https://api.apiryner.com/v1/bundles \
-H "Authorization: Bearer oat_your_token" \
-H "Content-Type: application/json" \
-d '{"name": "Shop", "slug": "shop", "layout_ids": [12, 13]}'
Snapshots
A snapshot freezes one generated dataset so every call returns the same bytes — for demos, screenshots and fixtures that must not move under you.
curl -X POST https://api.apiryner.com/v1/snapshots \
-H "Authorization: Bearer oat_your_token" \
-H "Content-Type: application/json" \
-d '{"layout_slug": "users", "name": "Demo users",
"slug": "demo-users", "count": 20, "seed": 42}'
GET /v1/snapshots/:slugserves the stored data, and?format=csvworks as it does on a layout.shape—auto(default),objectorarray— decides whether the snapshot answers with a single object or an array. Auto follows the layout.PUT /v1/snapshots/:slugreplacesnameordata, so pinned records can be hand-edited.- Serving a snapshot still counts against your monthly record quota — pinned data is data the API delivers.
Webhooks
Subscribe an HTTP(S) URL to account events. The creation response carries the signing secret once; it is never returned again.
curl -X POST https://api.apiryner.com/v1/webhooks \
-H "Authorization: Bearer oat_your_token" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/apiryner",
"events": ["layout.updated", "usage.warning"]}'
Pass layout_ids to limit layout events to specific layouts; omit it to receive them for every layout.
Events
| Event | Fires when |
|---|---|
| layout.created | A layout is created — through the API, the web editor or an MCP tool call |
| layout.updated | A layout’s name, slug, schema or generation defaults change |
| layout.deleted | A layout is deleted |
| snapshot.created | A snapshot is pinned |
| snapshot.deleted | A snapshot is deleted |
| bundle.created | A bundle is created |
| usage.warning | Monthly requests or records pass 80% of the limit |
| usage.limit_reached | Monthly requests or records reach the limit |
Delivery & verification
Each delivery is a JSON POST carrying two headers: X-Apiryner-Event with the event name, and X-Apiryner-Signature with an HMAC-SHA256 of the raw request body, keyed with your secret and hex-encoded. Verify it before trusting the payload:
const expected = crypto
.createHmac("sha256", secret)
.update(rawBody) // the raw body, before JSON.parse
.digest("hex");
if (expected !== req.headers["x-apiryner-signature"]) {
return res.status(401).end();
}
Delivery times out after 10 seconds. Network errors and 408/429/5xx responses are retried twice — after 5 and 30 seconds — while any other 4xx is final. Every attempt is recorded; read them back with GET /v1/webhooks/:id/deliveries.
Options Reference
Each field in a schema can have a type and optional options object.
Schema field structure:
{
"field_name": {
"type": "integer",
"options": {
"min": 1,
"max": 100
}
}
}
Every pool-backed type — name, email, phone, the address types, the text types and slug — also accepts a locale option that overrides the request’s locale for that one field. A field that overrides the locale generates its own identity rather than sharing the record’s person:
{
"name": {"type": "name"},
"korean_name": {"type": "name", "options": {"locale": "ko"}}
}
Nested object example:
{
"address": {
"type": "object",
"properties": {
"street": {"type": "street"},
"city": {"type": "city"},
"zipcode": {"type": "zipcode"}
}
}
}
Array example:
{
"tags": {
"type": "array",
"items": {"type": "word"},
"options": {"min_items": 2, "max_items": 5}
}
}
Enum example:
{
"status": {
"type": "enum",
"options": {
"values": ["active", "inactive", "pending"]
}
}
}
Errors & Limits
Errors come back as JSON with an error code and, in most cases, a human-readable message:
{
"error": "forbidden",
"message": "API keys are read-only. Use an OAuth Bearer token for write operations."
}
| Status | error | When |
|---|---|---|
| 400 | invalid request | Malformed body, an invalid slug, or a schema the generator cannot read |
| 401 | unauthorized | Missing, invalid, inactive or revoked credentials |
| 403 | forbidden | A write attempted with an API key — API keys are read-only |
| 403 | insufficient_scope | The OAuth token lacks the read or write scope (also sent as WWW-Authenticate) |
| 403 | layout_limit_reached | Your plan’s layout limit is already reached |
| 404 | <resource> not found | No layout, bundle, snapshot or template with that slug on your account |
| 429 | rate_limit_exceeded | Per-minute request rate exceeded — Retry-After says when to retry |
| 429 | quota_exceeded | The monthly request or record limit is used up |
Rate limits & quota
Every response carries the per-minute budget:
X-RateLimit-Limit— requests allowed per minute on your planX-RateLimit-Remaining— how many are left in the current windowX-RateLimit-Reset— when the window resets, as a Unix timestampRetry-After— seconds to wait, sent only with a 429
Monthly quota is counted in two dimensions, requests and records; a bundle call counts as one request plus every record it returns. Read the current period with GET /v1/account/usage:
{
"requests": 128,
"records": 4210,
"limit_requests": 1000,
"limit_records": 10000,
"period": "2026-08"
}
A count above your plan’s per-request maximum is capped rather than rejected, so a request never fails for asking too much. Layout, API key and nesting-depth limits are enforced the same way, at the point of use.
MCP Server
AI coding agents — Claude Code, Cursor, Claude Desktop — can generate mock data and manage your layouts through Model Context Protocol tool calls. Connect to the hosted endpoint, or run the server locally from the CLI.
Remote server — nothing to install
Point your agent at the hosted endpoint and authorize it in the browser. No npm package, no API key to copy.
claude mcp add --transport http apiryner https://api.apiryner.com/mcp
Your agent will report that the server needs authentication; approving it in the browser is the whole setup. To use an API key instead of the browser flow:
claude mcp add --transport http apiryner https://api.apiryner.com/mcp \
--header "X-API-Key: ak_your_key_here"
Local server
Runs the server from the apiryner CLI on your own machine. Its schema tools work offline, before you have an account.
Add it to Claude Code:
claude mcp add apiryner -- npx apiryner mcp
Or configure it by JSON (mcpServers):
{
"mcpServers": {
"apiryner": {
"command": "npx",
"args": ["apiryner", "mcp"],
"env": { "APIRYNER_API_KEY": "ak_your_key_here" }
}
}
}
Tools
| Tool | Auth | Arguments |
|---|---|---|
| list_types | optional | — |
| validate_schema | — | schema |
| infer_schema | — | input, type |
| quick_generate | — | schema, count, seed |
| list_layouts | required | limit, offset |
| create_layout | required | name, slug, schema, locale, is_array, array_count |
| update_layout | required | slug plus the fields to change |
| delete_layout | required | slug |
| generate | required | slug, count, seed, locale, format |
| generate_bundle | required | slug |
| get_snapshot | required | slug |
infer_schema and generate_bundle are on the local server only for now.
Authentication
validate_schema, infer_schema, and quick_generate run entirely on your machine — no account, no network. An agent can build fixtures or turn an OpenAPI file into a layout schema before you have signed up.
list_types is what keeps an agent from inventing field types. Authenticated, it returns this page’s full type catalog with descriptions and supported locales; unauthenticated, it falls back to the type names built into the package rather than failing.
The remote server authorizes through your browser: your agent registers itself, you approve it once, and the token it receives is bound to this endpoint alone. An X-API-Key header works too, and keeps the read-only limits an API key always has.
For the local server, credentials are resolved on the first tool call that needs the API, in this order:
--key <key>passed afterapiryner mcp, or$APIRYNER_API_KEY- the OAuth session from
apiryner login, refreshed automatically when expired - the API key stored by
apiryner config set api-key <key>
Starting the server unauthenticated is fine: the account-backed tools explain what is missing and begin working the moment you log in, with no restart. Note that create_layout, update_layout and delete_layout write to your account, and that a delete cannot be undone.
Try It Out
Test the API directly in your browser — no account needed.
Open the playgroundWant the full playground without caps? Sign up for free