> ## Documentation Index
> Fetch the complete documentation index at: https://docs.privy.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Create an app with an organization secret

> Use an organization secret to create and list apps through the organization API.

This guide shows how to create a Privy app with an organization secret.

Use this flow to provision apps for an existing organization without the Dashboard UI.

## Before you begin

Prerequisites:

* An existing Privy organization
* The `organization_secrets` feature enabled for that organization
* Admin access in the Privy Dashboard
* The organization ID
* An organization secret for that organization

## Create an organization secret

In the Privy Dashboard, open the organization settings and create an organization secret.

<Warning>
  Privy does not store organization secrets. Copy the value at creation time and store it in a
  secret manager.
</Warning>

Notes:

* Privy uses the organization ID for API authentication.
* Each organization can have at most 2 active organization secrets at a time.
* A secret can include an optional P-256 signing public key.
* When a signing key is present, every request with that secret must include a valid request
  signature.

## Create an app

Send a `POST` request to:

```text theme={"system"}
https://auth.privy.io/api/v1/organizations/{organization_id}/apps
```

The request must include:

* `Authorization: Basic <base64(organization_id:organization_secret)>`
* `privy-organization-id: <organization_id>`
* `Content-Type: application/json`

### Example with cURL

```bash theme={"system"}
export PRIVY_ORGANIZATION_ID="org_123"
export PRIVY_ORGANIZATION_SECRET="organization-secret-value"

curl --request POST \
  --url "https://auth.privy.io/api/v1/organizations/${PRIVY_ORGANIZATION_ID}/apps" \
  --header "Authorization: Basic $(printf '%s:%s' "$PRIVY_ORGANIZATION_ID" "$PRIVY_ORGANIZATION_SECRET" | base64)" \
  --header "privy-organization-id: ${PRIVY_ORGANIZATION_ID}" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "My Programmatic App"
  }'
```

### Example response

```json theme={"system"}
{
  "app_id": "clx123exampleapp",
  "app_name": "My Programmatic App",
  "app_secret": "privy_app_secret_123"
}
```

<Warning>
  Store the returned `app_secret` immediately. Like app secrets created in the Dashboard, it cannot
  be recovered later.
</Warning>

## List apps for an organization

List all apps in the organization with:

```bash theme={"system"}
curl --request GET \
  --url "https://auth.privy.io/api/v1/organizations/${PRIVY_ORGANIZATION_ID}/apps" \
  --header "Authorization: Basic $(printf '%s:%s' "$PRIVY_ORGANIZATION_ID" "$PRIVY_ORGANIZATION_SECRET" | base64)" \
  --header "privy-organization-id: ${PRIVY_ORGANIZATION_ID}"
```

Example response:

```json theme={"system"}
{
  "data": [
    {
      "app_id": "clx123exampleapp",
      "app_name": "My Programmatic App"
    }
  ]
}
```

## Configure the app

After app creation, use the returned `app_id` and `app_secret` to configure the app.

Use this app update endpoint:

```text theme={"system"}
https://auth.privy.io/api/v1/apps/{app_id}
```

This request uses app credentials, not organization credentials. Include:

* `Authorization: Basic <base64(app_id:app_secret)>`
* `privy-app-id: <app_id>`
* `Content-Type: application/json`

### Enable email and phone login methods

```bash theme={"system"}
export PRIVY_APP_ID="app-id"
export PRIVY_APP_SECRET="app-secret"

curl --request POST \
  --url "https://auth.privy.io/api/v1/apps/${PRIVY_APP_ID}" \
  --header "Authorization: Basic $(printf '%s:%s' "$PRIVY_APP_ID" "$PRIVY_APP_SECRET" | base64)" \
  --header "privy-app-id: ${PRIVY_APP_ID}" \
  --header "Content-Type: application/json" \
  --data '{
    "email_auth": true,
    "sms_auth": true
  }'
```

### Example response

```json theme={"system"}
{
  "id": "clx123exampleapp",
  "email_auth": true,
  "sms_auth": true
}
```

<Note>
  `POST /api/v1/apps/{app_id}` is a broad legacy app-settings endpoint. For this flow, the relevant
  fields are `email_auth` and `sms_auth`.
</Note>

<Note>
  SMS login cannot be enabled when SMS transaction MFA is already enabled for the app. SMS and
  WhatsApp are also mutually exclusive.
</Note>

## Signed requests

If the organization secret has a signing public key, include two extra headers on every request:

* `privy-request-expiry`: a future Unix timestamp in milliseconds
* `privy-authorization-signature`: a base64-encoded P-256 signature over the canonical request
  payload

Sign this canonical JSON payload:

```json theme={"system"}
{
  "version": 1,
  "method": "POST",
  "url": "https://auth.privy.io/api/v1/organizations/org_123/apps",
  "body": {
    "name": "My Programmatic App"
  },
  "headers": {
    "privy-organization-id": "org_123",
    "privy-request-expiry": "1760000000000"
  }
}
```

### Node.js example

This example uses [`canonicalize`](https://www.npmjs.com/package/canonicalize). It matches Privy's
canonical request format.

```ts theme={"system"}
import canonicalize from 'canonicalize';
import crypto from 'node:crypto';

const organizationId = process.env.PRIVY_ORGANIZATION_ID!;
const organizationSecret = process.env.PRIVY_ORGANIZATION_SECRET!;
const privateKeyPem = process.env.PRIVY_SIGNING_PRIVATE_KEY!;

const url = `https://auth.privy.io/api/v1/organizations/${organizationId}/apps`;
const requestExpiry = String(Date.now() + 60_000);
const body = {name: 'My Programmatic App'};

const payload = canonicalize({
  version: 1,
  method: 'POST',
  url,
  body,
  headers: {
    'privy-organization-id': organizationId,
    'privy-request-expiry': requestExpiry
  }
});

if (!payload) {
  throw new Error('Failed to canonicalize request payload');
}

const signer = crypto.createSign('SHA256');
signer.update(payload);
signer.end();

const signature = signer.sign(privateKeyPem, 'base64');
const basicAuth = Buffer.from(`${organizationId}:${organizationSecret}`).toString('base64');

const response = await fetch(url, {
  method: 'POST',
  headers: {
    Authorization: `Basic ${basicAuth}`,
    'Content-Type': 'application/json',
    'privy-organization-id': organizationId,
    'privy-request-expiry': requestExpiry,
    'privy-authorization-signature': signature
  },
  body: JSON.stringify(body)
});

console.log(await response.json());
```

## Troubleshooting

* If the organization ID in Basic auth does not match the organization ID in the URL, Privy
  rejects the request.
* If `privy-organization-id` is missing or does not match the authenticated organization, Privy
  rejects the request.
* A revoked organization secret causes Privy to reject the request.
* Enabling request signing on a secret causes Privy to reject requests without a valid
  `privy-authorization-signature`.
