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

# Exchange an authorization code for a bot token

> Redeems the `code` the consent screen redirected with. Installs the App
into the approving admin's company, or reconnects an existing
installation with the same bot user, and returns a new bot token. Any
earlier token of the installation stops working.

The token does not expire, so the response has no `expires_in` or
`refresh_token`. Codes are single-use and expire 10 minutes after
approval. Redeeming a used code again is `invalid_grant` and leaves the
token it already returned working, so retrying a timed-out exchange is
safe.




## OpenAPI

````yaml /openapi/dev.json post /oauth/token
openapi: 3.0.0
info:
  contact:
    email: support@workchats.com
    name: Workchats API Support
  description: >
    Post messages into Workchats as an installed App's bot user, and read

    the people and conversations it can reach.


    Authenticate with the bot token from the OAuth install, in the

    Authorization header as `Bearer wc_bot_…`. Every error response has

    the shape `{"ok": false, "error": {"code", "message"}, "request_id"}`.


    Lists are cursor-paginated: pass `limit` (default 200, at most 1000)

    and the previous page's `next_cursor` as `cursor`. `next_cursor` is

    null on the last page.


    ## Callbacks


    Workchats POSTs events to the App's registered callback URL, over

    HTTPS only. Answer with any 2xx within 3 seconds. Anything else,

    including a redirect, which is never followed, is retried after 1, 5

    and 30 minutes with the same event `id`, so ignore an `id` you have

    already handled. `X-Workchats-Attempt` counts the attempts from 1.


    Every event has the shape `{"id", "type", "workspace_id",

    "created_at", "data"}`:


    - `app.uninstalled`: `data` is `{"removed_by": {"id": "usr_…"} | null,
      "reason": "admin" | "app_disabled"}`. A company admin uninstalled
      the App, or Workchats disabled it (`removed_by` is null). The token
      has already stopped working. Revoking the token yourself through
      `POST /oauth/revoke` sends nothing.
    - `directory.changed`: `data` is `{"kind", "id"}`. Re-read the user
      with `GET /v1/users/{id}` or the conversation with
      `GET /v1/conversations/{id}`. Kinds: `user.created`,
      `user.updated` (name, email or title), `user.deactivated`
      (deactivated or removed), `conversation.created` (the bot was
      added), `conversation.renamed`, `conversation.archived` (groups),
      `conversation.deleted`, `app.removed_from_conversation`. User kinds
      need `users:read`; conversation kinds name only conversations the
      bot is, or was just, in. Ignore kinds you don't know.

    **Verifying a callback.** `X-Workchats-Signature` is `v1=` and the

    lower-case hex HMAC-SHA256 of `<X-Workchats-Timestamp>.<raw body>`,

    keyed with the App's signing secret. Compare in constant time, and

    refuse a timestamp more than 5 minutes old. For 24 hours after the

    secret is rotated the header carries two signatures,

    `v1=<new>,v1=<old>`: accept the request if any one matches.


    Worked example:


    - Signing secret: `whsec_docs_example`

    - X-Workchats-Timestamp: 1790593600

    - Body:
    `{"id":"evt_5f0c7a52-6d1e-4c1b-9a8e-2f4b7c3d1e90","type":"directory.changed","workspace_id":"cmp_0b7e4c1a-2d3f-4a5b-8c6d-7e8f9a0b1c2d","created_at":"2026-09-28T11:06:40Z","data":{"kind":"user.deactivated","id":"usr_9a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"}}`

    - X-Workchats-Signature:
    v1=42ec825e4ad44370ea02385d30a001c349640cff974ed895361b3c7a080e5339
  title: Workchats Public API
  version: '1.0'
servers:
  - url: https://public-api.dev.workchats.com
    variables: {}
security:
  - bearerAuth: []
tags:
  - description: Token checks
    name: Auth
  - description: Groups and channels the bot is a member of, and their messages
    name: Conversations
  - description: Files attached to messages the bot can read
    name: Files
  - description: Send, edit and delete the bot's messages
    name: Messages
  - description: Code exchange and token revocation
    name: OAuth
  - description: The workspace's people
    name: Users
paths:
  /oauth/token:
    post:
      tags:
        - OAuth
      summary: Exchange an authorization code for a bot token
      description: |
        Redeems the `code` the consent screen redirected with. Installs the App
        into the approving admin's company, or reconnects an existing
        installation with the same bot user, and returns a new bot token. Any
        earlier token of the installation stops working.

        The token does not expire, so the response has no `expires_in` or
        `refresh_token`. Codes are single-use and expire 10 minutes after
        approval. Redeeming a used code again is `invalid_grant` and leaves the
        token it already returned working, so retrying a timed-out exchange is
        safe.
      operationId: WorkchatsApiServerWeb.PublicApi.OAuthController.token
      parameters: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PublicApiTokenRequest'
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/PublicApiTokenRequest'
        description: Code exchange
        required: true
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiTokenResponse'
          description: The bot token
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiError'
          description: >-
            `invalid_request` (a parameter is missing or malformed),
            `unsupported_grant_type` (anything but `authorization_code`),
            `invalid_grant` (the code is unknown, expired, used or issued to
            another client, `redirect_uri` differs from the authorization
            request, or the approving admin lost the admin role or API access)
            or `token_in_query`
        '401':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiError'
          description: '`invalid_client`: client authentication failed'
        '406':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiError'
          description: '`not_acceptable`: the Accept header does not allow application/json'
        '429':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiError'
          description: >-
            `rate_limited`: more than 20 calls a minute for this App, or 20
            unauthenticated calls a minute from this IP; retry after
            `Retry-After` seconds
        '500':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PublicApiError'
          description: '`internal_error`: something went wrong on our side'
      callbacks: {}
      security:
        - {}
        - basicAuth: []
components:
  schemas:
    PublicApiTokenRequest:
      description: >-
        An authorization-code exchange. Send `client_id` and `client_secret`
        here or as HTTP Basic credentials.
      properties:
        client_id:
          type: string
        client_secret:
          type: string
        code:
          description: The code from the consent redirect
          type: string
        grant_type:
          enum:
            - authorization_code
          type: string
        redirect_uri:
          description: The redirect_uri of the authorization request, exactly
          format: uri
          type: string
      required:
        - grant_type
        - code
        - redirect_uri
      title: PublicApiTokenRequest
      type: object
    PublicApiTokenResponse:
      description: A non-expiring bot token for the installation.
      example:
        access_token: wc_bot_live_0Xa9…
        bot_user_id: usr_8a1d2c3e-4b5f-4a6b-8c7d-9e0f1a2b3c4d
        scope: users:read users:read.email conversations:read messages:write
        token_type: Bearer
        workspace:
          id: cmp_3f0c1b9e-5d0a-4a57-9a53-2f4f1c9d6b10
          name: Corpwise
      properties:
        access_token:
          description: '`wc_bot_live_…` or `wc_bot_test_…`'
          type: string
        bot_user_id:
          description: The bot's user id, `usr_` prefixed
          type: string
        scope:
          description: Granted scopes, space-separated
          type: string
        token_type:
          enum:
            - Bearer
          type: string
        workspace:
          properties:
            id:
              description: Company id, `cmp_` prefixed
              type: string
            name:
              type: string
          required:
            - id
            - name
          type: object
      required:
        - access_token
        - token_type
        - scope
        - workspace
        - bot_user_id
      title: PublicApiTokenResponse
      type: object
    PublicApiError:
      description: Every non-2xx response from the public API.
      example:
        error:
          code: token_revoked
          message: The token has been revoked.
        ok: false
        request_id: F7p0mJcXk3tq2x0AAAAB
      properties:
        error:
          properties:
            code:
              description: >-
                Stable, machine-readable error code. Each code always comes with
                the same HTTP status, except `client_error` and `server_error`,
                which carry the response's own 4xx or 5xx status. New codes may
                be added; treat an unknown one as a generic failure.
              type: string
              x-extensible-enum:
                - cannot_dm_user
                - client_error
                - conversation_archived
                - conversation_not_found
                - file_not_found
                - idempotency_key_reused
                - internal_error
                - invalid_auth
                - invalid_blocks
                - invalid_client
                - invalid_cursor
                - invalid_grant
                - invalid_request
                - message_not_found
                - missing_idempotency_key
                - missing_scope
                - not_acceptable
                - not_allowed_in_conversation
                - not_authed
                - not_found
                - not_in_conversation
                - payload_too_large
                - rate_limited
                - server_error
                - token_in_query
                - token_revoked
                - unsupported_grant_type
                - unsupported_media_type
                - user_deactivated
                - user_not_found
            message:
              description: Human-readable explanation
              type: string
          required:
            - code
            - message
          type: object
        ok:
          enum:
            - false
          type: boolean
        request_id:
          description: Also sent as the X-Request-Id header. Quote it to support.
          type: string
      required:
        - ok
        - error
        - request_id
      title: PublicApiError
      type: object
  securitySchemes:
    bearerAuth:
      description: Bot token (`wc_bot_live_…` or `wc_bot_test_…`) from the OAuth install
      scheme: bearer
      type: http
    basicAuth:
      description: The App's client_id and client_secret, for the OAuth endpoints
      scheme: basic
      type: http

````