# From a server

> Create submissions from a backend, script or integration with a secret API key.

Backends, cron jobs and integrations can post to the same endpoint as browsers. Add a secret API key so Ikiform knows the post is yours.

## Create a secret key

1. In the dashboard, open **API keys** and create a **secret** key.
2. Give it the `submissions:write` scope. Add `submissions:read` and `forms:read` only if the same key also reads data from the [REST API](https://ikiform.com/docs/api.md).
3. Copy the key. It starts with `ik_sk_` and is shown once; Ikiform stores only its hash.

## Send a submission

**Node** (`submit.ts`)

```ts
const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
    "Idempotency-Key": order.id,
  },
  body: JSON.stringify({ email: order.email, plan: order.plan }),
});

const result = await response.json();
if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message} (${result.request_id})`);
```

**curl** (`Terminal`)

```bash
curl https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx \
  -H "Authorization: Bearer $IKIFORM_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order_1234" \
  -d '{"email":"ada@example.com","plan":"team"}'
```

**Python** (`submit.py`)

```python
import os

import requests

response = requests.post(
    "https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx",
    json={"email": "ada@example.com", "plan": "team"},
    headers={
        "Authorization": f"Bearer {os.environ['IKIFORM_SECRET_KEY']}",
        "Idempotency-Key": "order_1234",
    },
    timeout=10,
)
result = response.json()
if not result["ok"]:
    raise RuntimeError(f"{result['error']['code']}: {result['error']['message']}")
```

## What a secret key changes

| Check | Without a key | With a secret key |
| --- | --- | --- |
| Allowed domains | Checked against `Origin` or `Referer` | Skipped |
| Ikiform human check | Required when turned on | Skipped |
| Your own Turnstile keys | Token required | Token still required |
| Rate limits | Per visitor IP and form | Still apply, per your server's IP |
| Field validation and spam scoring | Run | Run |

A key from another workspace, a revoked key, or a secret key without `submissions:write` gets `403 forbidden`. A malformed `Authorization` header is also refused rather than ignored, so a typo never silently downgrades to an anonymous post.

> Posts sent from your server all come from your server's IP address, so Ikiform's per-visitor rate limits count every visitor together. The Standard preset allows 5 posts a minute per IP and form, which a busy site can hit. Post from the browser when you can, or choose the Relaxed or Custom [rate limit](https://ikiform.com/docs/rate-limits.md) for forms you proxy through a server.

## Publishable keys

Publishable keys (`ik_pub_`) are for browsers. They can only submit, and only from the domains you list on the key. A post with a publishable key from any other site gets `403 origin_not_allowed`. They don't skip the human check or the form's own allowed domains.

> Never put a secret key in browser code, a mobile app or a public repository. Revoke it in the dashboard if it leaks.

## Retries

Servers retry. Send an `Idempotency-Key` that is stable for the thing you are submitting, such as an order id, and a retry will replay the first response instead of creating a duplicate. See [Idempotency](https://ikiform.com/docs/frameworks.md#idempotency).
