# Create analysis

Post a chart analysis card, with direction, price levels and optional drawings, into a chart-enabled channel on behalf of a member.

- Endpoint: `POST https://api.returning.ai/v1/analysis/{communityId}/channels/{channelId}`
- Section: Community / Chart analysis
- Authentication: `Authorization: Bearer <API_KEY>` (Community API key)
- Permission: `sendMessage` (Shown in the dashboard as "Send Messages")
- Retries: Not idempotent; list the channel before retrying
- Guide: hand-written
- Verified: code, 2026-09-27
- Last updated: 26 Sep 2026
- Web page: https://docs.returning.ai/api-reference/chart-analysis/create-analysis

## When to use this

- Your analysts write trade ideas in your own tools, and you want each one to appear as a chart card in your community.
- You publish a daily outlook for a symbol, with pivot, support and resistance levels drawn on the chart.
- You want a member's analysis to count toward their XP and appear in every member's activity feed.

**Instead:** Use [List analyses](https://docs.returning.ai/api-reference/chart-analysis/list-analyses.md) instead to check what a channel already shows before you post.

**Posts publicly:** A successful create posts the card in the channel as the sender, adds it to every member's activity feed and fires your message webhooks. There is no draft mode.

## Authentication

- Header: `Authorization: Bearer <API_KEY>`
- Permission: `sendMessage`

Use a Community API key with `sendMessage`, shown in the dashboard as Send Messages, and keep it on your server. The analysis is saved in the community that owns the key, whatever `communityId` you put in the path, so only post to your own channels. With a Community API key you must name the member in `sender`; the card appears as their post. Personal API keys are not accepted here and return `401`.

## Behaviour

The channel must have Enable Charts turned on, and `metadata.symbol` must be the chart symbol chosen in that channel's settings. The sender must be a member of your community with permission to post in the channel. After the body is validated, the checks run in this order: the channel exists, `sender` is given and found, Enable Charts is on, the sender may post, and the symbol matches. Nothing is posted when one fails.

A successful create does all of this at once: it posts the card in the channel as the sender, shows it live to members who have the channel open, adds a trade idea to every member's activity feed, may award the sender XP and coins under your community's rules, and fires your message webhooks.

Every new analysis starts as `Pending`. You can update it, or add drawings to it, only while it stays `Pending`.

## Request

### Path parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `communityId` | `string` | Yes | Your community's ID. The analysis is always saved in the community that owns your API key. (24 hex characters) |
| `channelId` | `string` | Yes | The channel to post in. It must have Enable Charts turned on in its channel settings. (24 hex characters) |

### Headers

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `Authorization` | `string` | Yes | Community API key with `sendMessage`. (`Bearer <API_KEY>`) |
| `Content-Type` | `string` | Yes | Request body format. (`application/json`) |

### Body

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `sender` | `string` | Yes | Username or email of the member the card is posted as. They must belong to your community and be allowed to post in the channel. |
| `metadata` | `object` | Yes | What the analysis says. |
| `metadata.direction` | `string` | Yes | Market bias. (`BULLISH` or `BEARISH`) |
| `metadata.entryPlacement` | `string` | Yes | Where the entry sits against the current price. With `direction` it sets the card type: bullish and `ABOVE_CURRENT` is a bullish breakout, bullish and `BELOW_CURRENT` a bullish bounce, bearish and `ABOVE_CURRENT` a bearish bounce, bearish and `BELOW_CURRENT` a bearish breakout. (`ABOVE_CURRENT` or `BELOW_CURRENT`) |
| `metadata.expiry` | `object` | Yes | How long the idea stands. |
| `metadata.expiry.type` | `string` | Yes | `GTC` (good till cancelled) or `HOURS`. (`GTC` or `HOURS`) |
| `metadata.expiry.hours` | `integer` | No | Hours the idea stands. Required when `type` is `HOURS`. (1-8760) |
| `metadata.title` | `string` | Yes | Card title. (1-200 characters) |
| `metadata.preferenceText` | `string` | Yes | Your main scenario. `[[clientObjectId:text]]` links a phrase to a level or drawing, which is highlighted on hover. (1-5000 characters) |
| `metadata.alternativeText` | `string` | No | The alternative scenario. Supports the same links. (Up to 5000 characters) |
| `metadata.comments` | `string` | No | Extra comments. Supports the same links. (Up to 5000 characters) |
| `metadata.timeframe` | `string` | Yes | Chart timeframe. (`M1`, `M5`, `M15`, `M30`, `H1`, `H2`, `H4`, `D`, `W` or `M`) |
| `metadata.symbol` | `string` | Yes | The channel's chart symbol, set in its channel settings. Not case-sensitive; saved in upper case. |
| `levels` | `object` | Yes | Price levels on the card. Each level has a `price`, a `clientObjectId` unique among the levels, and an optional ISO 8601 `time`. Order must be support, intermediate support, pivot, intermediate resistance, resistance, from lowest to highest price. |
| `levels.pivot` | `object` | Yes | The centre level. |
| `levels.pivot.price` | `number` | Yes | Price of the level. |
| `levels.pivot.clientObjectId` | `string` | Yes | Your own name for the level, unique among the levels. Use it in text links. (Min 1 chars) |
| `levels.pivot.time` | `string` | No | Optional chart time for the level. (ISO 8601 date-time) |
| `levels.resistance` | `object` | Yes | Main resistance, above the pivot. |
| `levels.resistance.price` | `number` | Yes | Price of the level. |
| `levels.resistance.clientObjectId` | `string` | Yes | Your own name for the level, unique among the levels. Use it in text links. (Min 1 chars) |
| `levels.resistance.time` | `string` | No | Optional chart time for the level. (ISO 8601 date-time) |
| `levels.support` | `object` | Yes | Main support, below the pivot. |
| `levels.support.price` | `number` | Yes | Price of the level. |
| `levels.support.clientObjectId` | `string` | Yes | Your own name for the level, unique among the levels. Use it in text links. (Min 1 chars) |
| `levels.support.time` | `string` | No | Optional chart time for the level. (ISO 8601 date-time) |
| `levels.intermediateResistance` | `object` | No | Optional level between the pivot and resistance. |
| `levels.intermediateResistance.price` | `number` | Yes | Price of the level. |
| `levels.intermediateResistance.clientObjectId` | `string` | Yes | Your own name for the level, unique among the levels. Use it in text links. (Min 1 chars) |
| `levels.intermediateResistance.time` | `string` | No | Optional chart time for the level. (ISO 8601 date-time) |
| `levels.intermediateSupport` | `object` | No | Optional level between support and the pivot. |
| `levels.intermediateSupport.price` | `number` | Yes | Price of the level. |
| `levels.intermediateSupport.clientObjectId` | `string` | Yes | Your own name for the level, unique among the levels. Use it in text links. (Min 1 chars) |
| `levels.intermediateSupport.time` | `string` | No | Optional chart time for the level. (ISO 8601 date-time) |
| `drawings` | `object[]` | No | Optional chart drawings, up to 20. Each `clientObjectId` must be unique among the drawings. (0-20 items) |
| `drawings.type` | `string` | Yes | Drawing type. `PARALLEL` needs exactly 3 points; `FIB_RETRACEMENT` needs 2 or 3. (`HORIZONTAL_LINE`, `LINE`, `RECTANGLE`, `PARALLEL` or `FIB_RETRACEMENT`) |
| `drawings.clientObjectId` | `string` | Yes | Your own name for the drawing. Use it in text links, and to find the drawing again in the response. (Min 1 chars) |
| `drawings.points` | `object[]` | Yes | Chart points for the drawing. (3 for `PARALLEL`, 2-3 for `FIB_RETRACEMENT`) |
| `drawings.points.time` | `string \| integer` | Yes | Point time. (ISO 8601 date-time in UTC ending in `Z`, or epoch milliseconds) |
| `drawings.points.price` | `number` | Yes | Point price. |
| `drawings.startHead` | `string` | No | `LINE` only: the start decoration. (`NONE`, `ARROW` or `EXTEND`; default `NONE`) |
| `drawings.endHead` | `string` | No | `LINE` only: the end decoration. (`NONE`, `ARROW` or `EXTEND`; default `NONE`) |
| `drawings.levels` | `number[]` | No | `FIB_RETRACEMENT` only: the ratios to draw. Omit for the standard set from -0.618 to 1.618. |
| `drawings.style` | `object` | No | Optional colours and line style. |
| `drawings.style.color` | `string` | No | Line colour. (`#RRGGBB`) |
| `drawings.style.lineWidth` | `integer` | No | Line width. (1-10) |
| `drawings.style.lineStyle` | `string` | No | Line style. (`SOLID`, `DASHED` or `DOTTED`) |
| `drawings.style.fillColor` | `string` | No | Fill colour. An alpha channel such as `#00FF0033` is rejected. (`#RRGGBB`) |
| `drawings.style.borderColor` | `string` | No | Border colour. (`#RRGGBB`) |
| `drawings.style.opacity` | `number` | No | Opacity. (0-1) |
| `idempotencyKey` | `string` | No | Accepted but not used. It does not prevent a duplicate card, so list the channel before you retry. |

### Example request

```bash
curl --request POST \
  --url https://api.returning.ai/v1/analysis/66f000000000000000000010/channels/66f000000000000000000b11 \
  --header 'Authorization: Bearer <API_KEY>' \
  --header 'Content-Type: application/json' \
  --data '{
    "sender": "trader@example.com",
    "metadata": {
      "direction": "BULLISH",
      "entryPlacement": "BELOW_CURRENT",
      "expiry": {
        "type": "HOURS",
        "hours": 24
      },
      "title": "EURUSD bounce from support",
      "preferenceText": "Price is holding above [[support_main:support]]. Look for longs toward [[resistance_main:resistance]].",
      "timeframe": "H1",
      "symbol": "EURUSD"
    },
    "levels": {
      "pivot": {
        "price": 1.1,
        "clientObjectId": "pivot_main",
        "time": "2026-09-27T08:00:00.000Z"
      },
      "resistance": {
        "price": 1.12,
        "clientObjectId": "resistance_main",
        "time": "2026-09-27T08:00:00.000Z"
      },
      "support": {
        "price": 1.08,
        "clientObjectId": "support_main",
        "time": "2026-09-27T08:00:00.000Z"
      }
    },
    "drawings": [
      {
        "type": "HORIZONTAL_LINE",
        "clientObjectId": "entry_line",
        "points": [
          {
            "time": "2026-09-27T08:00:00.000Z",
            "price": 1.095
          }
        ],
        "style": {
          "color": "#2E7D32"
        }
      }
    ]
  }'
```

## Response

A `200` (not `201`) means the card is posted. Save `data.analysisId`. `data.objectIdMap` tells you the saved ID behind each `clientObjectId` you sent; you need your own `clientObjectId` values again when you add drawings, because they must not repeat.

### Response fields

| Field | Type | Presence | Description |
| --- | --- | --- | --- |
| `status` | `string` | always | Result of the request. (`success`) |
| `message` | `string` | always | Human-readable summary. Do not branch on it. |
| `data` | `object` | always | The new analysis. |
| `data.analysisId` | `string` | always | The analysis ID. Use it to read, update, extend or delete the analysis. |
| `data.objectIdMap` | `object` | always | Each `clientObjectId` you sent, mapped to its saved ID. Levels map to the card ID plus a position, such as `<cardId>:8` for the pivot; drawings map to their own IDs. |

### Example response (200)

```json
{
  "status": "success",
  "message": "Analysis created successfully",
  "data": {
    "analysisId": "66f000000000000000000b01",
    "objectIdMap": {
      "pivot_main": "66f000000000000000000b41:8",
      "support_main": "66f000000000000000000b41:0",
      "resistance_main": "66f000000000000000000b41:7",
      "entry_line": "66f000000000000000000b31"
    }
  }
}
```

## Errors

These errors have no machine-readable code; branch on the HTTP status and read `message`. The `401` for a missing or unknown key has `detail` and a `meta` object; the `401` for a missing permission has only `message`. A validation `400` names each field in `detail`. Other errors carry `status` and `message`.

### Fix the request

| Status | Code | What to do |
| --- | --- | --- |
| 400 | - | The request was rejected before anything was posted. Read the body: - `Create Analysis Request Validation validation error.`: a field is missing or breaks its rule, such as a lower-case `direction`, levels out of order or repeated `clientObjectId` values. `detail` names each field. - `Sender field is required`: send `sender`. - `Channel does not support analysis posts (chartsEnabled is false)`: turn on Enable Charts in the channel's settings, or use a chart channel. - `Symbol <SYMBOL> is not supported by this channel. Allowed symbols: ...`: send the channel's chart symbol. |
| 401 | - | The key is missing (`Token is require.`) or unknown (`Token is wrong.`), it's a personal key, or it lacks `sendMessage` (`Your API key does not have permission to access this action`). Send `Authorization: Bearer <API_KEY>` with a Community API key, and add Send Messages in Settings > Integration > API Keys. |

### Fix the data

| Status | Code | What to do |
| --- | --- | --- |
| 403 | - | The sender isn't allowed to post in this channel. Give the member, or one of their roles, permission to post there, or send as another member. |
| 404 | - | `User not found`: no member of your community has this username or email. `Channel not found`: no channel has this ID. Check the value and send again. |

### Retry with backoff

| Status | Code | What to do |
| --- | --- | --- |
| 500 | - | `Error creating analysis`: the create failed part way, and `detail` has the reason. List the channel before you retry, because the card may already be posted. A `500` with an HTML error page means a `[[clientObjectId:text]]` link names an ID that is not in `levels` or `drawings`; fix the link, since nothing was posted. |

**Retries:** This endpoint is not idempotent. `idempotencyKey` is accepted but ignored, and every successful call posts another card and notifies members again. After a timeout or a `500`, call List analyses for the channel, newest first, and look for your title before you send again.

## Next step

- [Read the new card back with Get analysis](https://docs.returning.ai/api-reference/chart-analysis/get-analysis.md): `GET /v1/analysis/{analysisId}`. Use `data.analysisId` to read the saved levels, drawings and status.
- [Append drawings](https://docs.returning.ai/api-reference/chart-analysis/append-drawings.md): `POST /v1/analysis/{analysisId}/drawings`. Add more drawings to the card while it is still `Pending`.
