agendaforge-app Jobs API

Async work: 202 Accepted imports and the pollable job resource.

Operations 3

POST /api/v1/imports/contacts Import up to 1,000 contacts asynchronously #
POST /api/v1/imports/tickets Add, update, or cancel up to 1,000 imported tickets #
GET /api/v1/jobs/{id} Poll an async job #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/agendaforge-app:agendaforge-app-jobs-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Discovery needs no key. Ratings and market analysis are Pro.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

agendaforge-app-jobs-api-openapi.yml Raw ↑
openapi: 3.2.0
info:
  title: AgendaForge Public REST Jobs API
  version: 1.0.0
  summary: 'Server-to-server REST API for the AgendaForge event-management platform: webhooks (REST Hooks), sample records, cursor-paginated list endpoints, create actions and batch create for contacts, sessions, and sponsors, and async contact and ticket imports with pollable jobs.'
  description: 'The HTTP API that powers the AgendaForge Zapier, Make.com, and n8n integrations, and any custom server-to-server integration.


    Every request is authenticated with an organization API key (created in Settings > API Tokens; keys begin with afk_live_ and are shown once). Keys carry named scopes chosen at creation: public_api (this API, on by default), mcp:read and mcp:write (the AgendaForge MCP server for AI clients), and tickets:import (ticket imports, never on by default). Every endpoint here requires the public_api scope, except POST /api/v1/imports/tickets, which requires tickets:import; requests with a key that lacks the required scope receive 403 FORBIDDEN.


    All requests and responses are JSON. Every error, 4xx and 5xx alike, is JSON of the shape { "error": { "code": "...", "message": "..." } } with a machine-readable code and a human-readable resolution hint. Unknown paths under /api/v1 return 404 NOT_FOUND with the list of valid endpoints.


    Pagination: list endpoints (GET /contacts, GET /sessions, GET /sponsors) are cursor-based. Request a page with ?limit= (1-100, default 25), then follow nextCursor via ?cursor= until hasMore is false. Cursors are opaque continuation tokens; never construct one.


    Long-running work: POST /api/v1/imports/contacts answers 202 Accepted with a job id and a statusUrl (also in the Location header); poll GET /api/v1/jobs/{id} until status reaches completed or failed. For up to 100 items in one synchronous call, use POST /api/v1/contacts/batch instead. POST /api/v1/imports/tickets finishes up to 100 tickets inside the request (200, job already completed) and queues 101 to 1,000 tickets (202, poll the same way).


    Rate limits: 120 requests per minute per API key (fixed window), and 30 requests per minute per IP for unauthenticated requests. Every response carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (seconds until the window resets), and RateLimit-Policy headers so clients can self-throttle; exceeding the window returns 429 RATE_LIMITED with a Retry-After header.


    Versioning and deprecation: the API is versioned in the URL path (/api/v1). Additive changes (new endpoints, new optional fields, new webhook events) happen within a version and are never breaking; breaking changes ship only as a new version path. If an endpoint or version is ever deprecated it will be announced in the changelog at least 180 days before removal and signaled with Deprecation and Sunset headers on affected responses. Nothing is deprecated today.


    Human-readable reference: https://agendaforge.app/docs/api. Developer portal: https://agendaforge.app/developers. Site content for agents: https://agendaforge.app/llms.txt.'
  contact:
    name: AgendaForge support
    email: hello@agendaforge.app
    url: https://agendaforge.app/docs/api
  termsOfService: https://agendaforge.app/terms
servers:
- url: https://api.agendaforge.app
  description: Production
security:
- bearerApiKey:
  - public_api
- headerApiKey:
  - public_api
tags:
- name: Jobs
  description: 'Async work: 202 Accepted imports and the pollable job resource.'
paths:
  /api/v1/imports/contacts:
    post:
      security:
      - bearerApiKey:
        - public_api
      - headerApiKey:
        - public_api
      x-required-scopes:
      - public_api
      operationId: importContacts
      tags:
      - Jobs
      summary: Import up to 1,000 contacts asynchronously
      description: 'Async job pattern: accepts the items, returns 202 Accepted immediately with a job id and statusUrl (also sent as the Location header), and processes the import in the background. Poll GET /api/v1/jobs/{id} until status is completed or failed; the job result carries created/failed counts and up to 50 per-item errors. Items behave exactly like single POST /api/v1/contacts requests.'
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportContactsRequest'
      responses:
        '202':
          description: The job was accepted. Poll statusUrl (also in the Location header) for progress.
          headers:
            Location:
              description: The poll URL, identical to statusUrl in the body.
              schema:
                type: string
                format: uri
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ImportJobAccepted'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/IdempotencyConflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        default:
          $ref: '#/components/responses/UnexpectedError'
  /api/v1/imports/tickets:
    post:
      security:
      - bearerApiKey:
        - tickets:import
      - headerApiKey:
        - tickets:import
      x-required-scopes:
      - tickets:import
      operationId: importTickets
      tags:
      - Jobs
      summary: Add, update, or cancel up to 1,000 imported tickets
      description: 'Sends ticket rows from your own ticketing system to one event, with the same rules as a file import in the app. Requires the tickets:import scope (public_api alone is not enough). Each row is matched by reference: a new reference creates a ticket and needs a name (name, or firstName and lastName) and a ticketType; a reference that already exists may carry only what changed. Rows not in the request are left alone. The event must be activated and set up for imported tickets, and only one import runs per event at a time. Up to 100 tickets are applied inside the request and answer 200 with the job already completed; 101 to 1,000 tickets answer 202 Accepted with a statusUrl (also sent as the Location header) to poll at GET /api/v1/jobs/{id}. Rows fail independently: a row problem counts toward result.failed and is listed in result.errors (first 50) without stopping the other rows. Whole-request problems (an unreadable body, a bad field type, duplicate or too-short scanKey values, more than 50 ticket types) answer 400 and nothing is imported. Besides the per-key limit, each event accepts 12 ticket import requests per minute. With an Idempotency-Key, a repeat of the same request returns the original response (status, body, and Location) with Idempotent-Replay: true, not current progress; poll statusUrl for that.'
      parameters:
      - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        description: A JSON object of at most 2 MB.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ImportTicketsRequest'
            examples:
              add:
                summary: Add a ticket
                value:
                  eventId: k17e8m2q5n9p3r6s1t4v
                  tickets:
                  - reference: b7c1e0aa-0000-4000-8000-00000000a001
                    name: Ana Okafor
                    email: ana@forgelabs.example
                    company: Forge Labs
                    title: Head of Platform
                    ticketType: General
                    status: valid
                    scanKey: 5d2f9c10-0000-4000-8000-00000000b001
              cancel:
                summary: Cancel a ticket (send only the reference and what changed)
                value:
                  eventId: k17e8m2q5n9p3r6s1t4v
                  tickets:
                  - reference: b7c1e0aa-0000-4000-8000-00000000a001
                    status: cancelled
      responses:
        '200':
          description: 'Up to 100 tickets: the import finished inside the request. The job is completed and result carries the final counts.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TicketsImportJob'
              example:
                jobId: jb7q2m5k8n1r4s7t0v3x
                type: tickets_import
                status: completed
                total: 1
                statusUrl: https://api.agendaforge.app/api/v1/jobs/jb7q2m5k8n1r4s7t0v3x
                result:
                  total: 1
                  processed: 1
                  created: 1
                  updated: 0
                  unchanged: 0
                  cancelled: 0
                  restored: 0
                  failed: 0
                  errors: []
        '202':
          description: '101 to 1,000 tickets: the import was accepted and runs in the background. Poll statusUrl (also in the Location header) until status is completed or failed.'
          headers:
            Location:
              description: The poll URL, identical to statusUrl in the body.
              schema:
                type: string
                format: uri
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TicketsImportJob'
        '400':
          $ref: '#/components/responses/TicketImportValidationError'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '402':
          $ref: '#/components/responses/PaymentRequired'
        '403':
          $ref: '#/components/responses/TicketImportForbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/TicketImportConflict'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        default:
          $ref: '#/components/responses/UnexpectedError'
  /api/v1/jobs/{id}:
    get:
      security:
      - bearerApiKey:
        - public_api
      - headerApiKey:
        - public_api
      - bearerApiKey:
        - tickets:import
      - headerApiKey:
        - tickets:import
      x-required-scopes:
      - public_api
      - tickets:import
      operationId: getJob
      tags:
      - Jobs
      summary: Poll an async job
      description: Returns the current status and result counters of a job created by POST /api/v1/imports/contacts or POST /api/v1/imports/tickets. A contacts_import job needs a key with public_api; a tickets_import job needs a key with tickets:import. Jobs are retained for 7 days; unknown, expired, and cross-organization ids all answer 404. Poll with backoff (a 1,000-item import typically completes within a minute).
      parameters:
      - name: id
        in: path
        required: true
        description: The jobId returned by the 202 response.
        schema:
          type: string
      responses:
        '200':
          description: The job with live progress counters. The shape depends on the job type.
          content:
            application/json:
              schema:
                oneOf:
                - $ref: '#/components/schemas/Job'
                - $ref: '#/components/schemas/TicketsImportJob'
        '400':
          $ref: '#/components/responses/ValidationError'
        '401':
          $ref: '#/components/responses/Unauthenticated'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
        default:
          $ref: '#/components/responses/UnexpectedError'
components:
  schemas:
    CreateContactRequest:
      type: object
      required:
      - firstName
      - lastName
      - email
      properties:
        type:
          type: string
          description: speaker | sponsor | attendee | vendor | partner | exhibitor | other. Defaults to "attendee".
        firstName:
          type: string
        lastName:
          type: string
        email:
          type: string
          format: email
        phone:
          type: string
        company:
          type: string
        jobTitle:
          type: string
    Error:
      type: object
      required:
      - error
      description: Every non-2xx response carries this JSON shape. The code is machine-readable; the message is a human-readable resolution hint.
      properties:
        error:
          type: object
          required:
          - code
          - message
          properties:
            code:
              type: string
              enum:
              - UNAUTHENTICATED
              - FORBIDDEN
              - VALIDATION_ERROR
              - PAYMENT_REQUIRED
              - NOT_FOUND
              - RATE_LIMITED
              - CONFLICT
              - INTERNAL
            message:
              type: string
    Job:
      type: object
      required:
      - jobId
      - type
      - status
      - createdAt
      - completedAt
      - result
      properties:
        jobId:
          type: string
        type:
          type: string
          enum:
          - contacts_import
        status:
          type: string
          enum:
          - pending
          - processing
          - completed
          - failed
          description: Terminal states are completed and failed; keep polling until one is reached.
        createdAt:
          type: string
          description: ISO-8601 UTC timestamp.
        completedAt:
          type:
          - string
          - 'null'
          description: ISO-8601 UTC timestamp; null until the job reaches a terminal state.
        result:
          type: object
          required:
          - total
          - processed
          - created
          - failed
          - errors
          properties:
            total:
              type: integer
            processed:
              type: integer
              description: Items handled so far; equals total when done.
            created:
              type: integer
            failed:
              type: integer
            errors:
              type: array
              description: Per-item failures (first 50), each with the failing input index.
              items:
                type: object
                required:
                - index
                - code
                - message
                properties:
                  index:
                    type: integer
                  code:
                    type: string
                  message:
                    type: string
    ImportJobAccepted:
      type: object
      required:
      - jobId
      - type
      - status
      - total
      - statusUrl
      properties:
        jobId:
          type: string
          description: Opaque job id.
        type:
          type: string
          enum:
          - contacts_import
        status:
          type: string
          enum:
          - pending
        total:
          type: integer
          description: Items accepted for processing.
        statusUrl:
          type: string
          format: uri
          description: Poll this URL (GET, same authentication) for progress.
    ImportTicketsRequest:
      type: object
      required:
      - eventId
      - tickets
      properties:
        eventId:
          type: string
          description: The event to import into; must be owned by the key's organization, activated, and set up for imported tickets.
        tickets:
          type: array
          minItems: 1
          maxItems: 1000
          items:
            $ref: '#/components/schemas/TicketImportRow'
        confirmCheckedInCancels:
          type: boolean
          default: false
          description: A ticket that has already been checked in is cancelled or refunded only when this is true. Otherwise that row fails with the checked_in code.
    TicketImportRow:
      type: object
      required:
      - reference
      description: One ticket. Unknown fields are ignored. A new reference needs a name (name, or firstName and lastName) and a ticketType; a reference that already exists may carry only what changed. Row problems fail only that row.
      properties:
        reference:
          type: string
          maxLength: 128
          description: Your system's ticket or order line id. It is how a later request updates or cancels the same ticket, so keep it stable.
        name:
          type: string
          description: Full name; split into first and last name. Ignored when firstName or lastName is sent. 120 characters at most.
        firstName:
          type: string
        lastName:
          type: string
        email:
          type: string
          format: email
          maxLength: 254
        company:
          type: string
          maxLength: 160
        title:
          type: string
          maxLength: 160
          description: Job title.
        ticketType:
          type: string
          maxLength: 80
          description: Ticket type name. An unknown name creates the type. At most 50 different values per request.
        status:
          type: string
          description: 'Case-insensitive. Valid: valid, active, paid, complete, completed, confirmed, attending, issued. Cancelled: cancelled, canceled, void, voided. Refunded: refunded, refund. Any other word fails the row with unknown_status.'
        table:
          type: string
          maxLength: 40
        seat:
          type: string
          maxLength: 20
        scanKey:
          type: string
          minLength: 8
          maxLength: 128
          description: Optional. The value inside the attendee's existing QR code, so it scans at the door. Must be unique within the event. Stored only as a hash.
        purchasedAt:
          type:
          - string
          - number
          description: Purchase date as a date string (e.g. 2026-11-04) or a millisecond timestamp.
        answers:
          type: array
          maxItems: 12
          description: Registration answers to keep with the ticket.
          items:
            type: object
            required:
            - label
            - value
            properties:
              label:
                type: string
                description: Question label; up to 120 characters are kept.
              value:
                type: string
                description: Answer; up to 300 characters are kept.
    TicketsImportJob:
      type: object
      required:
      - jobId
      - type
      - status
      - total
      - statusUrl
      - result
      description: Returned by POST /api/v1/imports/tickets and by GET /api/v1/jobs/{id} for a tickets_import job.
      properties:
        jobId:
          type: string
          description: Opaque job id.
        type:
          type: string
          enum:
          - tickets_import
        status:
          type: string
          enum:
          - pending
          - processing
          - completed
          - failed
          description: completed in a 200 response; pending in a 202 response. Terminal states are completed and failed; keep polling until one is reached.
        total:
          type: integer
          description: Tickets in the request.
        statusUrl:
          type: string
          format: uri
          description: Poll this URL (GET, a key with tickets:import) for progress.
        result:
          type: object
          required:
          - total
          - processed
          - created
          - updated
          - unchanged
          - cancelled
          - restored
          - failed
          - errors
          properties:
            total:
              type: integer
            processed:
              type: integer
              description: Rows handled so far, failed rows included; equals total when done.
            created:
              type: integer
              description: New tickets.
            updated:
              type: integer
              description: Existing tickets that changed.
            unchanged:
              type: integer
              description: Existing tickets with nothing to change.
            cancelled:
              type: integer
              description: Tickets cancelled or refunded.
            restored:
              type: integer
              description: Cancelled or refunded tickets made valid again.
            failed:
              type: integer
              description: Rows not applied.
            errors:
              type: array
              description: Row failures (first 50). code is one of missing_reference, reference_too_long, reference_mangled, reference_reserved, duplicate_reference, missing_name, invalid_email, missing_ticket_type, unknown_status, scan_key_reserved, scan_key_length, scan_key_taken, invalid_date, field_too_long, missing_fields, checked_in, event_full, type_catalogue_full. A job that stops early reports one CONFLICT entry and status failed.
              items:
                type: object
                required:
                - index
                - reference
                - code
                - message
                properties:
                  index:
                    type: integer
                    description: Zero-based position in the tickets array.
                  reference:
                    type:
                    - string
                    - 'null'
                    description: The row's reference, or null when it is withheld.
                  code:
                    type: string
                  message:
                    type: string
    ImportContactsRequest:
      type: object
      required:
      - contacts
      properties:
        contacts:
          type: array
          minItems: 1
          maxItems: 1000
          items:
            $ref: '#/components/schemas/CreateContactRequest'
  responses:
    TicketImportValidationError:
      description: The body is not a JSON object of at most 2 MB, eventId is missing, tickets is not an array of 1 to 1,000 objects, a field has the wrong type, or the rows break a whole-request rule (duplicate or too-short scanKey values, more than 50 ticket types, more than 12 answers or an over-long value in a row). Nothing was imported.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: VALIDATION_ERROR
              message: Send between 1 and 1,000 tickets in one request.
    PaymentRequired:
      description: The event is not activated yet. Ticket imports need an activated event.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: PAYMENT_REQUIRED
              message: This event isn't activated yet. Contact us to activate it for publishing, registrations, and AI.
    TicketImportForbidden:
      description: The key lacks the tickets:import scope, or the event does not belong to the key's organization.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: FORBIDDEN
              message: This API key cannot import tickets. Create a key with the ticket import permission.
    ValidationError:
      description: A required field is missing or invalid, an unknown event was supplied, or a referenced record is not owned by the key's organization.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: VALIDATION_ERROR
              message: firstName, lastName and email are required.
    TicketImportConflict:
      description: Another import is already running for this event, the event is not set up for imported tickets, or the Idempotency-Key was already used for a different endpoint or a different request body.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: CONFLICT
              message: An import is already running for this event. Wait for it to finish.
    IdempotencyConflict:
      description: The Idempotency-Key was already used on a different endpoint within the 24 hour retention window.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: CONFLICT
              message: This Idempotency-Key was already used on a different endpoint. Use a new key.
    Unauthenticated:
      description: The API key is missing, malformed, invalid, expired, or revoked.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: UNAUTHENTICATED
              message: 'Missing API key. Send ''Authorization: Bearer <key>''.'
    UnexpectedError:
      description: Any other failure, including 404 NOT_FOUND for an unknown path and transient 5xx errors. Always the same JSON error contract.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: INTERNAL
              message: Unexpected server error. Retry with exponential backoff; if it persists, contact hello@agendaforge.app.
    InternalError:
      description: An unexpected server failure. The body still follows the JSON error contract; retry with exponential backoff.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: INTERNAL
              message: Unexpected server error. Retry with exponential backoff; if it persists, contact hello@agendaforge.app.
    RateLimited:
      description: The fixed window for this API key (120 requests per minute) or IP (30 per minute, unauthenticated) is exhausted. Wait Retry-After seconds and retry.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: RATE_LIMITED
              message: Rate limit exceeded. Retry after the number of seconds in the Retry-After header.
      headers:
        Retry-After:
          description: Seconds to wait before retrying (RFC 6585).
          schema:
            type: string
        RateLimit-Limit:
          description: Requests allowed per window. Sent on every /api/v1 response, not only 429s.
          schema:
            type: string
        RateLimit-Remaining:
          description: Requests left in the current window.
          schema:
            type: string
        RateLimit-Reset:
          description: Seconds until the current window resets.
          schema:
            type: string
        RateLimit-Policy:
          description: The policy as "<limit>;w=<window seconds>", e.g. "120;w=60".
          schema:
            type: string
    NotFound:
      description: No such endpoint under /api/v1. The message lists the valid endpoints.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: NOT_FOUND
              message: No such endpoint. See https://agendaforge.app/openapi.json for the API surface.
    Forbidden:
      description: The API key is valid but does not include the public_api scope. Create a key with the public_api scope in Settings > API Tokens.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error:
              code: FORBIDDEN
              message: This API key does not include the "public_api" scope. Create a key with the public_api scope in Settings > API Tokens.
  parameters:
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: 'Optional key for safe retries, retained for 24 hours. Repeating the same request on the same endpoint returns the original response and sets Idempotent-Replay: true. Reusing the key on a different endpoint returns 409.'
      schema:
        type: string
        minLength: 1
        maxLength: 255
  securitySchemes:
    bearerApiKey:
      type: http
      scheme: bearer
      bearerFormat: afk_live_ organization API key
      description: 'Organization API key sent as "Authorization: Bearer afk_live_...". Keys are created in Settings > API Tokens with named scopes (see x-api-key-scopes): public_api grants this REST API; mcp:read and mcp:write grant the AgendaForge MCP server; tickets:import grants ticket imports. Every endpoint here requires the public_api scope except POST /api/v1/imports/tickets, which requires tickets:import.'
      x-scopes:
      - public_api
      - mcp:read
      - mcp:write
      - tickets:import
    headerApiKey:
      type: apiKey
      in: header
      name: X-API-Key
      description: The same afk_live_ organization API key sent as an X-API-Key header instead of a Bearer token. Identical scope semantics to bearerApiKey.
      x-scopes:
      - public_api
      - mcp:read
      - mcp:write
      - tickets:import
externalDocs:
  description: AgendaForge API reference (human-readable)
  url: https://agendaforge.app/docs/api
x-api-key-scopes:
- name: public_api
  description: Grants the public REST API v1 (this specification). On by default when a key is created. Required by every endpoint here except POST /api/v1/imports/tickets; a key without it receives 403 FORBIDDEN.
- name: mcp:read
  description: Grants read tools on the AgendaForge MCP server for AI clients (events, sessions, contacts, forms). MCP keys act as the admin who created them and are re-checked against live membership on every call.
- name: mcp:write
  description: 'Grants write tools on the AgendaForge MCP server. Every write is approval-gated: the AI proposes, a person approves.'
- name: tickets:import
  description: Grants POST /api/v1/imports/tickets and polling its jobs at GET /api/v1/jobs/{id}. Labeled "Import tickets" in Settings > API Tokens. Never on by default and never implied by another scope; it does not grant the other endpoints here.