---
title: "Create a Circle"
method: POST
path: "/api/v1/circles"
tags: ["Circles"]
---

# Create a Circle

`POST /api/v1/circles`

## Request

- Base URL: `https://api.proof.holdings`
- URL: `https://api.proof.holdings/api/v1/circles`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `auth_token`

Create a named group of trusted contacts. Initial members may declare channels inline.

## Request body

- CreateCircleBody: Request body for creating a Circle
  - `name` string, required
  - `owner_name` string, nullable: How this Circle's members know its owner ("Dad", "Giorgi"). The bots name the owner by it; while it is empty they name nobody — the Circle's `name` labels the group, never the person. Trimmed; an empty value clears it.
  - `profile_id` string: Optional public profile to bind for branded messages (create-only). The caller must own it (403 otherwise).
  - `proof_me_enabled` boolean: Opt this Circle into Proof of Me drills
  - `members` CircleMemberInput[]: Optional initial members; each declared channel is written as a `declared` channel doc
    - `name` string, required
    - `telegram_chat_id` string: Optional pre-declared Telegram chat id (usually captured at enrollment instead)
    - `whatsapp_phone` string: Optional pre-declared WhatsApp phone in E.164 (leading +)

## Response `201`

Circle created

- Circle: A Circle — a named group of trusted contacts
  - `id` string, required: MongoDB ObjectId
  - `user_id` string, required: MongoDB ObjectId
  - `name` string, nullable, required
  - `owner_name` string, nullable, required: How this Circle's members know its owner; null when unset
  - `profile_id` string, required: MongoDB ObjectId
  - `environment` 'production' | 'test', required
  - `proof_me_enabled` boolean, required
  - `status` 'active' | 'archived', required: Lifecycle state of a Circle (archived = soft-retired)
  - `member_count` integer, required
  - `members` CircleMember[], required
    - `id` string, required: MongoDB ObjectId
    - `name` string, required
    - `status` 'pending' | 'active', required
    - `has_telegram` boolean, required: Whether the member has a Telegram channel (declared or verified)
    - `has_whatsapp` boolean, required: Whether the member has a WhatsApp channel (declared or verified)
    - `mesh_opted_in` boolean, required
    - `mesh_opt_out` boolean, required: The member sent /opt_out: they receive no drills, reminders or checks they did not start themselves
    - `enrolled_phone_hint` string, nullable, required: Masked form of the phone the contact proved when they enrolled (the Telegram shared contact or the WhatsApp sender) — country prefix and last four digits only, never the full number. Lets the owner see who actually joined. Null until the member enrolls, and for members enrolled before it was recorded.
    - `enrolled_at` string, date-time, nullable, required
    - `last_verified_at` string, date-time, nullable, required: When this member last CONFIRMED an identity check asked of them — i.e. last proved it was them. Null until they have.
  - `created_at` string, date-time, required
  - `updated_at` string, date-time, required

## Other responses

- `400`: Validation error
- `401`: Unauthorized
- `403`: `profile_mismatch` — a project-bound API key named another sub-account; it acts for its own sub-account only. Profile not owned
- `429`: Rate limit exceeded

---

[API](https://skmtc.dev/proof/apis/proof-holdings-api.md) · [All operations](https://skmtc.dev/proof/apis/proof-holdings-api/llms.txt) · [OpenAPI document](https://skmtc.dev/proof/apis/proof-holdings-api/revisions/6b8d61ee354f?raw)
