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

# Create a campaign

> Create a campaign with its steps, lead source lists and sending seats. A campaign is the **campaign agent** — the outreach half of a **Full Cycle agent**: create the list first (`POST /v1/list`), then the source agent that fills it with leads (`POST /v1/agent`), then this campaign to reach out to them. **At least one list (lead source) is required.** The campaign reaches out to the contacts of its lists; lists are typically filled by a source agent (the lead finder, see `POST /v1/agent`) or by importing contacts. If the campaign contains any LinkedIn step (invitation, invitationNote, message, voiceMessage, visitProfile, likePosts), a `linkedinSeatId` is required. If it contains email steps, at least one `emailSeatId` is required; multiple email seats (up to 100) are only allowed when the campaign has email steps exclusively. Step rules: at most one Connection Request step, an invitation cannot come after a LinkedIn message or voice message, and `delayAfterLastStep` must be at least 1 day (see the `steps` field for per-type fields and limits). When the campaign has a Connection Request step, 1st-degree connections are excluded from the campaign by default. **Once activated, the campaign starts the outreach automatically on a daily basis.** The launch hours can be configured in the LinkedIn seat settings.



## OpenAPI

````yaml POST /v1/campaign
openapi: 3.0.0
info:
  title: Gojiberry AI - External API
  description: >-

    # Gojiberry AI External API


    This API allows you to programmatically access your Gojiberry AI data and
    integrate it with your applications.


    ## How it works


    Gojiberry runs your outreach as a pipeline of three building blocks:


    1. **Source agent — the lead finder.** A source agent continuously sources
    leads matching your targeting criteria (job titles, industries, locations,
    signals, ...) and scores them with AI.

    2. **List — where the leads land.** Every source agent is linked to a list
    (`listId` is required when creating an agent): the leads it finds are
    imported as contacts into that list. Lists can also be filled by importing
    contacts yourself.

    3. **Campaign — the outreach.** A campaign uses one or more lists as its
    lead source (`listIds` is required when creating a campaign) and runs its
    steps (LinkedIn invitations, messages, emails, ...) on the contacts of those
    lists, sending through your LinkedIn and/or email seats. Once activated, the
    campaign runs the outreach automatically on a daily basis.


    **Terminology.** In the Gojiberry platform, an **Agent** is a *campaign
    agent* — the campaign that performs the outreach. A **source agent** is the
    lead finder that sources the leads. The two are linked through a **list**:
    the source agent imports the leads it finds into the list, and the campaign
    agent reaches out to the contacts of that list.


    **Best practice: create a Full Cycle agent.** Always create the three
    resources in this order — first the list, then the source agent that fills
    it, then the campaign that reaches out to it. Together, the list + source
    agent + campaign agent form a **Full Cycle agent**: leads are sourced and
    outreached automatically, end to end. If you create a source agent without a
    campaign agent, it shows up as a **legacy agent** in the platform: it only
    sources leads and does **no outreach**. So it's best practice to always
    create all three.


    A typical setup through the API:


    ```text

    POST /v1/list                  # 1. create the list

    POST /v1/agent                 # 2. create the source agent (lead finder)
    linked to that list (listId)

    POST /v1/campaign              # 3. create the campaign agent using that
    list as lead source (listIds)

    GET  /v1/campaign/{id}/logs    # 4. follow the outreach activity

    ```


    ## Authentication


    All API requests require authentication using a Bearer token. To get your
    API key:


    1. **Log in to your Gojiberry AI account** at
    [https://app.gojiberry.ai](https://app.gojiberry.ai)

    2. **Navigate to Settings** → **API** section

    3. **Click "Create API Key"** to generate a new token

    4. **Copy the API key** when it's displayed (you won't be able to see it
    again)

    5. **Use the token** in your requests by adding the header: `Authorization:
    Bearer YOUR_API_KEY`


    ## Impersonation (organization owners)


    If your API key belongs to the **owner of an organization**, you can perform
    any API call on behalf of another member of your organization by adding the
    `x-impersonate-user-id` header with the member's user ID:


    ```bash

    # Get the contacts of the organization member with user ID 123

    curl -H "Authorization: Bearer YOUR_API_KEY" \
         -H "x-impersonate-user-id: 123" \
         https://ext.gojiberry.ai/v1/contact
    ```


    When impersonating, the request behaves exactly as if it was made by that
    member: you read and write their data (contacts, campaigns, lists, unibox,
    etc.).


    Rules:

    - Your API key must belong to the **organization owner**; other members
    cannot impersonate.

    - The impersonated user must belong to the **same organization** as the API
    key user.

    - Requests that don't meet these rules are rejected with a `401
    Unauthorized`.


    Tip: use `GET /v1/organization/members` to list the members of your
    organization and their user IDs, and `GET /v1/user/me` to verify which user
    the current request acts as.


    ## Rate Limits


    - **100 requests per minute** per account. Short bursts above this are
    tolerated.

    - **100,000 requests per day** per account.

    - Limits are shared by all API keys and AI app (MCP) connections of the same
    user, including calls made on behalf of organization members with
    `x-impersonate-user-id`.

    - Requests over a limit return `429 Too Many Requests` with a `Retry-After`
    header (in seconds). Wait that long before retrying.

    - Some endpoints have their own lower limit, stated in their description.


    ## Base URL

    - **Production**: `https://ext.gojiberry.ai`


    ## Endpoints


    ### Contacts

    - `GET /v1/contact` - Get all contacts with filtering and pagination

    - `GET /v1/contact/{id}` - Get a specific contact

    - `GET /v1/contact/health` - Health check endpoint


    ## Example Usage


    ```bash

    # Get all contacts

    curl -H "Authorization: Bearer YOUR_API_KEY" \
         https://ext.gojiberry.ai/v1/contact

    # Get contacts with filtering

    curl -H "Authorization: Bearer YOUR_API_KEY" \
         "https://ext.gojiberry.ai/v1/contact?search=john&agent=1&dateFrom=2024-01-01"

    # Get a specific contact

    curl -H "Authorization: Bearer YOUR_API_KEY" \
         https://ext.gojiberry.ai/v1/contact/123
    ```


    ## Webhooks


    Gojiberry allows you to receive real-time notifications when new contacts
    are created or updated by configuring a webhook URL.


    ### How to Configure Webhooks


    1. **Log in to your Gojiberry AI account** at
    [https://app.gojiberry.ai](https://app.gojiberry.ai)

    2. **Navigate to the Integrations page** at
    [https://app.gojiberry.ai/integrations](https://app.gojiberry.ai/integrations)

    3. **Add a Webhook integration** by clicking the Webhook option

    4. **Enter your webhook URL** where you want to receive notifications

    5. **Save the configuration**


    Once configured, Gojiberry will send POST requests to your webhook URL
    whenever:

    - A new contact is created

    - A contact is updated

    - Other relevant events occur in your account


    The webhook payload will include the full contact data in JSON format. Each
    webhook request includes a custom header `x-gojiberry-user-id` containing
    your user ID for verification purposes.


    ### Verifying Webhook Signatures (optional)


    You can optionally configure a signing secret on your Webhook integration.
    When a secret is set, every webhook request also includes:


    - `x-gojiberry-timestamp`: the Unix timestamp (in seconds) at which the
    request was sent

    - `x-gojiberry-signature`: `sha256=` followed by the hex-encoded HMAC-SHA256
    of `{timestamp}.{rawBody}` computed with your signing secret


    To verify a request:


    1. Read the raw request body as a string (do not re-serialize parsed JSON).

    2. Concatenate the value of `x-gojiberry-timestamp`, a `.` character, and
    the raw body.

    3. Compute the HMAC-SHA256 of that string using your signing secret,
    hex-encoded, and prefix it with `sha256=`.

    4. Compare it to `x-gojiberry-signature` using a constant-time comparison.

    5. Reject requests whose timestamp is too old (we recommend a 5 minute
    tolerance) to prevent replay attacks.


    Example in Node.js:


    ```js

    const crypto = require('crypto');


    function verifyGojiberrySignature(rawBody, headers, secret) {
      const timestamp = headers['x-gojiberry-timestamp'];
      const signature = headers['x-gojiberry-signature'];
      if (!timestamp || !signature) return false;
      if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
      const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(timestamp + '.' + rawBody).digest('hex');
      return expected.length === signature.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
    }

    ```


    If no signing secret is configured, these headers are not sent.


    ## Support


    For API support, please contact us at
    [hello@gojiberry.ai](mailto:hello@gojiberry.ai)
          
  version: '1.0'
  contact: {}
servers:
  - url: https://ext.gojiberry.ai
    description: Production
security: []
tags: []
paths:
  /v1/campaign:
    post:
      tags:
        - Campaigns
      summary: Create a campaign
      description: >-
        Create a campaign with its steps, lead source lists and sending seats. A
        campaign is the **campaign agent** — the outreach half of a **Full Cycle
        agent**: create the list first (`POST /v1/list`), then the source agent
        that fills it with leads (`POST /v1/agent`), then this campaign to reach
        out to them. **At least one list (lead source) is required.** The
        campaign reaches out to the contacts of its lists; lists are typically
        filled by a source agent (the lead finder, see `POST /v1/agent`) or by
        importing contacts. If the campaign contains any LinkedIn step
        (invitation, invitationNote, message, voiceMessage, visitProfile,
        likePosts), a `linkedinSeatId` is required. If it contains email steps,
        at least one `emailSeatId` is required; multiple email seats (up to 100)
        are only allowed when the campaign has email steps exclusively. Step
        rules: at most one Connection Request step, an invitation cannot come
        after a LinkedIn message or voice message, and `delayAfterLastStep` must
        be at least 1 day (see the `steps` field for per-type fields and
        limits). When the campaign has a Connection Request step, 1st-degree
        connections are excluded from the campaign by default. **Once activated,
        the campaign starts the outreach automatically on a daily basis.** The
        launch hours can be configured in the LinkedIn seat settings.
      operationId: CampaignExternalController_create
      parameters:
        - name: x-impersonate-user-id
          in: header
          description: >-
            ID of an organization member to act on behalf of. The API key user
            must be the owner of the organization and the target user must
            belong to the same organization.
          required: false
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCampaignExternalDto'
      responses:
        '201':
          description: Campaign created
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Campaign'
        '400':
          description: Invalid steps or seat configuration
        '404':
          description: List or seat not found
      security:
        - bearer: []
components:
  schemas:
    CreateCampaignExternalDto:
      type: object
      properties:
        name:
          type: string
          description: Campaign name
          example: Q1 Sales Campaign
        steps:
          type: array
          description: >-
            Campaign steps, executed in order. Valid types: invitation,
            invitationNote, message, voiceMessage, visitProfile, likePosts,
            email. Ordering rules: at most one Connection Request step
            (invitation or invitationNote); an invitation cannot come after a
            message or voiceMessage step. Common fields: delayAfterLastStep in
            days (integer >= 1, default 2). stepNumber is assigned automatically
            from the array order and unknown fields are ignored. message and
            email steps take a messageMode: 'ai' (content is generated by AI for
            each contact, the default when no content is provided) or 'same'
            (the provided content is sent as-is, supports the [FirstName],
            [LastName], [Company], [JobTitle], [SenderFirstName], [SenderName],
            [SenderFullName] and [SenderCompany] variables; an empty value
            removes the tag). In 'same' mode, message is required (max 1900
            chars for LinkedIn messages) and email steps also require subject.
            invitationNote requires note (max 180 chars, 280 with a Sales
            Navigator seat); voiceMessage requires url to the audio file;
            likePosts accepts numberOfPosts (1-3, default 1); visitProfile and
            invitation need no extra fields.
          example:
            - type: invitationNote
              note: Hi [FirstName], let’s connect!
              delayAfterLastStep: 1
            - type: message
              messageMode: ai
              delayAfterLastStep: 2
            - type: email
              subject: Quick question
              message: Hi [FirstName], ...
              delayAfterLastStep: 3
          items:
            type: object
        listIds:
          description: IDs of the lists used as lead source. At least one list is required.
          example:
            - 1
          type: array
          items:
            type: string
        linkedinSeatId:
          type: number
          description: >-
            LinkedIn seat sending the LinkedIn steps. Required when the campaign
            contains any LinkedIn step; not allowed on email-only campaigns.
          example: 1
        emailSeatIds:
          description: >-
            Email seats sending the email steps. Required when the campaign
            contains email steps. Multiple seats (up to 100) are only allowed on
            email-only campaigns. Use GET /v1/email-seat to list your email
            seats and their IDs.
          example:
            - 1
          type: array
          items:
            type: string
        active:
          type: boolean
          description: >-
            Whether the campaign starts active. Once active, the campaign starts
            the outreach automatically on a daily basis; the launch hours can be
            configured in the LinkedIn seat settings.
          example: true
          default: true
        language:
          type: string
          description: Campaign language
          example: en-US
      required:
        - name
        - steps
        - listIds
    Campaign:
      type: object
      properties:
        id:
          type: number
          description: Unique identifier for the campaign
        name:
          type: string
          description: Name of the campaign
        steps:
          type: array
          description: Campaign steps
          items:
            type: object
        userId:
          type: number
          description: User ID who owns this campaign
        active:
          type: boolean
          description: Whether the campaign is active
          default: true
        excludeFirstDegreeFromCampaigns:
          type: boolean
          description: Exclude first degree connections from campaigns
          default: true
        splitLinkedinMessagesIntoConversation:
          type: boolean
          description: >-
            When enabled, LinkedIn message steps may be sent as multiple shorter
            messages when it feels natural
          default: false
        isManual:
          type: boolean
          description: Whether the campaign is manual
          default: false
        skipInvitationAfterDays:
          type: number
          description: >-
            Number of days after which a pending invitation is skipped and only
            email steps are sent
        language:
          type: string
          description: Campaign language
          nullable: true
        tone:
          type: string
          description: Campaign tone
          enum:
            - professional
            - direct
            - conversational
        goal:
          type: string
          description: Campaign goal
          enum:
            - conversations
            - demos
        linkedinSeatId:
          type: number
          description: LinkedIn seat ID for this campaign
        createdAt:
          format: date-time
          type: string
          description: Creation timestamp
        updatedAt:
          format: date-time
          type: string
          description: Last update timestamp
      required:
        - id
        - name
        - userId
        - active
        - excludeFirstDegreeFromCampaigns
        - splitLinkedinMessagesIntoConversation
        - isManual
        - createdAt
        - updatedAt
  securitySchemes:
    bearer:
      scheme: bearer
      bearerFormat: JWT
      type: http
      description: 'API key from Settings → API. Send it as Authorization: Bearer <key>.'

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.