Skip to content
For developers

API documentation

Connect Convs to your own CRM, shop or data warehouse: read contacts and leads, change stages, send conversions to the Meta queue and receive events through webhooks.

Version
1.0.0
Base URL
https://app.convs.io/api/v1
On this page

Quick start

  1. Create a key in the panel: Konfiguracja → API (Settings → API). Owners and admins of the organisation can create keys. The full key is shown only once, so copy it right away.
  2. Every URL starts with https://app.convs.io/api/v1. Responses are JSON.
  3. Check the key with GET /me. It needs no scope and returns the organisation, the key name, its scopes and expiry.
curl
curl -H "Authorization: Bearer cvs_live_…" \
  https://app.convs.io/api/v1/me

The other examples read the key from the API_KEY environment variable (export API_KEY=cvs_live_…). Never put the key in website or mobile app code: it is for server-to-server calls only.

Authentication

Every request sends an organisation key in the Authorization: Bearer cvs_live_… header. A key belongs to one organisation and sees only its data.

The full key is shown once, when it is created. We store only its hash, so it cannot be recovered: revoke a lost key and create a new one.

You can revoke a key in the panel at any time and give it an expiry date. A revoked key gets 401 key_revoked, an expired one 401 key_expired.

A key has scopes. A :write scope (and leads:answers) includes :read of the same area. contacts:* and leads:answers return personal data. A missing scope answers 403 insufficient_scope with details.required_scope.

ScopeAllowsEndpoints
contacts:readRead people of the Klienci (Clients) page (names, phones, e-mails — personal data).
contacts:writeCreate and change people: details, stage, notes, tags, SMS block.
leads:readRead Meta Instant Forms leads, without form answers.
leads:answersLike leads:read, plus the form answers (answers) — personal data.
    events:readRead conversions and the status of their deliveries to Meta.
    events:writeSend server-to-server conversions to the queue.
    campaigns:readCampaign report.
    creatives:readCreative report.
    jobs:readRead background jobs.
    webhooks:readRead webhooks and their delivery history.
    webhooks:writeCreate, change, delete and test webhooks.

    Errors

    Every error has the same shape: {"error": {"code", "message", "details?"}}.

    code is stable and never translated, so base your error handling on it. message is for humans, in the language of the Accept-Language header: Polish by default, en for English. details appears only when there is extra data, e.g. required_scope.

    JSON
    {
      "error": {
        "code": "insufficient_scope",
        "message": "The API key lacks the contacts:write scope.",
        "details": {
          "required_scope": "contacts:write"
        }
      }
    }
    CodeHTTPMeaning
    unauthorized401Missing key, wrong format or unknown key.
    key_revoked401The key was revoked.
    key_expired401The key is past its expiry date.
    insufficient_scope403The key lacks the required scope (details.required_scope).
    rate_limited429Over 120 requests per minute for this key; wait Retry-After seconds.
    invalid_json400The body is not valid JSON or has an unknown field.
    validation_error400A field or parameter has an invalid value; the message says what to fix.
    invalid_cursor400cursor is not from the previous page of the same list.
    not_found404No such record in your organisation.
    route_not_found404No such endpoint.
    conflict409The operation conflicts with the record’s current state.
    contact_exists409A person with this phone or e-mail already exists (details.contact_id).
    contact_merged409The card was merged into another one (details.merged_into).
    stage_conflict409The stage cannot be changed this way right now.
    event_conflict409The event conflicts with one already accepted.
    source_paused409The source is paused.
    limit_reached409A limit was reached, e.g. 10 webhooks per organisation.
    idempotency_key_reused422The same Idempotency-Key with a different request body.
    idempotency_in_progress409A request with this Idempotency-Key is still running; retry shortly.
    payload_too_large413Request body over 64 KB.
    internal_error500An error on our side; retry later.

    Pagination

    Lists return {"data": [...], "next_cursor", "has_more"}. Set the page size with limit (1–100, default 25).

    Fetch the next page by passing the previous response’s next_cursor as cursor. When has_more is false, the list is complete.

    Records are ordered by updated_at, then id: order=desc newest first (default) or order=asc.

    updated_since (ISO 8601) returns only records changed at or after that time. For incremental sync, store the time you start a sync, page through to the end, and use the stored time as updated_since for the next sync.

    Every contact changed since a given time

    curl
    cursor=""
    while :; do
      page=$(curl -s -G "https://app.convs.io/api/v1/contacts" \
        -H "Authorization: Bearer $API_KEY" \
        --data-urlencode "limit=100" \
        --data-urlencode "updated_since=2026-10-01T00:00:00Z" \
        ${cursor:+--data-urlencode "cursor=$cursor"})
      echo "$page" | jq -c '.data[]'
      [ "$(echo "$page" | jq -r '.has_more')" = "true" ] || break
      cursor=$(echo "$page" | jq -r '.next_cursor')
    done
    JavaScript
    const records = []
    let cursor = null
    do {
      const params = new URLSearchParams({ limit: '100', updated_since: '2026-10-01T00:00:00Z' })
      if (cursor) params.set('cursor', cursor)
      const response = await fetch(`https://app.convs.io/api/v1/contacts?${params}`, {
        headers: { Authorization: `Bearer ${process.env.API_KEY}` },
      })
      if (!response.ok) throw new Error((await response.json()).error.message)
      const page = await response.json()
      records.push(...page.data)
      cursor = page.has_more ? page.next_cursor : null
    } while (cursor)
    Python
    import os
    
    import requests
    
    records = []
    params = {"limit": 100, "updated_since": "2026-10-01T00:00:00Z"}
    while True:
        response = requests.get(
            "https://app.convs.io/api/v1/contacts",
            headers={"Authorization": f"Bearer {os.environ['API_KEY']}"},
            params=params,
            timeout=30,
        )
        response.raise_for_status()
        page = response.json()
        records.extend(page["data"])
        if not page["has_more"]:
            break
        params["cursor"] = page["next_cursor"]
    PHP
    <?php
    $records = [];
    $params = ['limit' => 100, 'updated_since' => '2026-10-01T00:00:00Z'];
    do {
        $ch = curl_init('https://app.convs.io/api/v1/contacts?' . http_build_query($params));
        curl_setopt_array($ch, [
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('API_KEY')],
        ]);
        $page = json_decode(curl_exec($ch), true);
        curl_close($ch);
        if (isset($page['error'])) {
            throw new RuntimeException($page['error']['message']);
        }
        $records = array_merge($records, $page['data']);
        $params['cursor'] = $page['next_cursor'];
    } while ($page['has_more']);

    Limits and formats

    • Rate limit: 120 requests per minute per key. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.
    • Over the limit you get 429 rate_limited with a Retry-After header (seconds). Wait that long, then retry.
    • A request body can be at most 64 KB, otherwise 413 payload_too_large.
    • Times in responses are ISO 8601 in UTC, e.g. 2026-10-08T10:00:00Z.

    Idempotency

    • Every POST accepts an Idempotency-Key header (1–255 visible ASCII characters). Use a value tied to the operation, e.g. a UUID stored with the record on your side.
    • Repeating a request with the same key and the same body within 24 hours returns the stored response with Idempotent-Replayed: true, without running the operation again.
    • The same key with a different body answers 422 idempotency_key_reused. While the first request is still running you get 409 idempotency_in_progress.
    • 5xx and 429 responses are not stored, so after them you can safely retry with the same key.

    Endpoints

    Keys

    The calling key.

    GET/me

    Describe the calling key

    Organisation, key name, scopes and expiry. A safe first call to check a new key; needs no scope.

    Scope: no scope

    Responses
    • 200OKResponse fields
      NameDescription
      organizationobject
      organization.idstring
      organization.namestring
      keyobject
      key.idstring
      key.namestring
      key.prefixstring
      key.scopesstring[]
      key.expires_atstring (date-time) | null
      rate_limitobject
      rate_limit.per_minuteinteger
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/me" \
      -H "Authorization: Bearer $API_KEY"
    JavaScript
    const response = await fetch('https://app.convs.io/api/v1/me', {
      headers: {
        Authorization: `Bearer ${process.env.API_KEY}`,
      },
    })
    if (!response.ok) throw new Error((await response.json()).error.message)
    const data = await response.json()
    console.log(data)
    Python
    import os
    
    import requests
    
    response = requests.get(
        "https://app.convs.io/api/v1/me",
        headers={
            "Authorization": f"Bearer {os.environ['API_KEY']}",
        },
        timeout=30,
    )
    response.raise_for_status()
    print(response.json())
    PHP
    <?php
    $ch = curl_init('https://app.convs.io/api/v1/me');
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => 'GET',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . getenv('API_KEY'),
        ],
    ]);
    $data = json_decode(curl_exec($ch), true);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($status >= 400) {
        throw new RuntimeException($data['error']['message']);
    }
    print_r($data);

    Contacts

    People of the Klienci page.

    GET/contacts

    List contacts

    People of the Klienci page (merged cards are left out). Filter by stage, tag, exact e-mail or phone, or the ad they came from.

    Scope: contacts:read

    Parameters
    NameDescription
    limitqueryintegerPage size, 1–100 (default 25).Default: 25Range: 1–100
    cursorquerystringnext_cursor of the previous page.
    orderquerystringSort by updated_at (then id): desc newest first (default) or asc.Values: asc, descDefault: desc
    updated_sincequerystring (date-time)Only records changed at or after this ISO 8601 time.
    stagequerystringStage.Values: new, contacted, qualified, sold, rejected
    tagquerystringTag.
    emailquerystringExact e-mail (matched by hash).
    phonequerystringExact phone (matched by hash).
    campaignquerystringPeople with a lead from this Meta campaign id.
    adsetquerystring…from this ad set id.
    adquerystring…from this ad id.
    Responses
    • 200OKResponse fields
      NameDescription
      dataContact[]Required
      data[].idstring
      Contact id.
      data[].namestring
      Full name.
      data[].phonestring
      Phone in E.164 form, empty when unknown.
      data[].emailstring
      E-mail, empty when unknown.
      data[].stagestring
      Sales stage.Values: new, contacted, qualified, sold, rejected
      data[].cycleinteger
      Sales cycle number; a new lead for a closed person opens the next cycle.
      data[].tagsstring[]
      Tags.
      data[].sms_optoutboolean
      True when SMS to this person are blocked.
      data[].first_sourcestring
      Where the person first came from (form name, "Ręcznie"…).
      data[].first_adAdSource | null
      The first ad the person came from, null for organic or unknown.
      data[].first_ad.platformstring
      Placement platform reported by Meta (facebook, instagram…).
      data[].first_ad.campaign_idstring
      Meta campaign id.
      data[].first_ad.campaign_namestring
      Campaign name.
      data[].first_ad.adset_idstring
      Meta ad set id.
      data[].first_ad.adset_namestring
      Ad set name.
      data[].first_ad.ad_idstring
      Meta ad id.
      data[].first_ad.ad_namestring
      Ad name.
      data[].owner_idstring | null
      User id of the person's owner (opiekun).
      data[].last_activity_atstring (date-time) | null
      Last activity time.
      data[].created_atstring (date-time)
      Creation time.
      data[].updated_atstring (date-time)
      Last change; use with updated_since.
      next_cursorstring | nullRequired
      Pass as cursor to get the next page; null on the last page.
      has_morebooleanRequired
      True when another page exists.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/contacts" \
      -H "Authorization: Bearer $API_KEY"
    JavaScript
    const response = await fetch('https://app.convs.io/api/v1/contacts', {
      headers: {
        Authorization: `Bearer ${process.env.API_KEY}`,
      },
    })
    if (!response.ok) throw new Error((await response.json()).error.message)
    const data = await response.json()
    console.log(data)
    Python
    import os
    
    import requests
    
    response = requests.get(
        "https://app.convs.io/api/v1/contacts",
        headers={
            "Authorization": f"Bearer {os.environ['API_KEY']}",
        },
        timeout=30,
    )
    response.raise_for_status()
    print(response.json())
    PHP
    <?php
    $ch = curl_init('https://app.convs.io/api/v1/contacts');
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => 'GET',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . getenv('API_KEY'),
        ],
    ]);
    $data = json_decode(curl_exec($ch), true);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($status >= 400) {
        throw new RuntimeException($data['error']['message']);
    }
    print_r($data);
    POST/contacts

    Create a contact

    Adds a person like „Dodaj osobę” in the panel (round robin owner when it is on).

    Scope: contacts:write

    Parameters
    NameDescription
    Idempotency-KeyheaderstringUnique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with Idempotent-Replayed: true.Max length: 255
    Request body application/json
    NameDescription
    namestring
    Full name (up to 200 characters).Max length: 200
    phonestring
    Phone with a country code, e.g. +48 600 857 482, or 9 digits (Poland).
    emailstring
    E-mail address.Max length: 254
    tagsstring[]
    Tags.
    Responses
    • 201CreatedResponse fields
      NameDescription
      idstring
      Contact id.
      namestring
      Full name.
      phonestring
      Phone in E.164 form, empty when unknown.
      emailstring
      E-mail, empty when unknown.
      stagestring
      Sales stage.Values: new, contacted, qualified, sold, rejected
      cycleinteger
      Sales cycle number; a new lead for a closed person opens the next cycle.
      tagsstring[]
      Tags.
      sms_optoutboolean
      True when SMS to this person are blocked.
      first_sourcestring
      Where the person first came from (form name, "Ręcznie"…).
      first_adAdSource | null
      The first ad the person came from, null for organic or unknown.
      first_ad.platformstring
      Placement platform reported by Meta (facebook, instagram…).
      first_ad.campaign_idstring
      Meta campaign id.
      first_ad.campaign_namestring
      Campaign name.
      first_ad.adset_idstring
      Meta ad set id.
      first_ad.adset_namestring
      Ad set name.
      first_ad.ad_idstring
      Meta ad id.
      first_ad.ad_namestring
      Ad name.
      owner_idstring | null
      User id of the person's owner (opiekun).
      last_activity_atstring (date-time) | null
      Last activity time.
      created_atstring (date-time)
      Creation time.
      updated_atstring (date-time)
      Last change; use with updated_since.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 409Conflicts with the current state (conflict, contact_exists, contact_merged, stage_conflict, event_conflict, source_paused, idempotency_in_progress).
    • 413Body over 64 KB (payload_too_large).
    • 422Idempotency-Key reused with a different body (idempotency_key_reused).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl -X POST "https://app.convs.io/api/v1/contacts" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Anna Nowak",
        "phone": "+48600100200",
        "email": "anna@example.com",
        "tags": [
          "vip"
        ]
      }'
    JavaScript
    const response = await fetch('https://app.convs.io/api/v1/contacts', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.API_KEY}`,
        'Idempotency-Key': '5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        "name": "Anna Nowak",
        "phone": "+48600100200",
        "email": "anna@example.com",
        "tags": [
          "vip"
        ]
      }),
    })
    if (!response.ok) throw new Error((await response.json()).error.message)
    const data = await response.json()
    console.log(data)
    Python
    import os
    
    import requests
    
    response = requests.post(
        "https://app.convs.io/api/v1/contacts",
        headers={
            "Authorization": f"Bearer {os.environ['API_KEY']}",
            "Idempotency-Key": "5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b",
        },
        json={
            "name": "Anna Nowak",
            "phone": "+48600100200",
            "email": "anna@example.com",
            "tags": ["vip"],
        },
        timeout=30,
    )
    response.raise_for_status()
    print(response.json())
    PHP
    <?php
    $ch = curl_init('https://app.convs.io/api/v1/contacts');
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => 'POST',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . getenv('API_KEY'),
            'Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b',
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'name' => 'Anna Nowak',
            'phone' => '+48600100200',
            'email' => 'anna@example.com',
            'tags' => ['vip'],
        ]),
    ]);
    $data = json_decode(curl_exec($ch), true);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($status >= 400) {
        throw new RuntimeException($data['error']['message']);
    }
    print_r($data);
    GET/contacts/{id}

    Get a contact

    A merged card answers 409 contact_merged with details.merged_into.

    Scope: contacts:read

    Parameters
    NameDescription
    idpathstringRequiredRecord id.
    Responses
    • 200OKResponse fields
      NameDescription
      idstring
      Contact id.
      namestring
      Full name.
      phonestring
      Phone in E.164 form, empty when unknown.
      emailstring
      E-mail, empty when unknown.
      stagestring
      Sales stage.Values: new, contacted, qualified, sold, rejected
      cycleinteger
      Sales cycle number; a new lead for a closed person opens the next cycle.
      tagsstring[]
      Tags.
      sms_optoutboolean
      True when SMS to this person are blocked.
      first_sourcestring
      Where the person first came from (form name, "Ręcznie"…).
      first_adAdSource | null
      The first ad the person came from, null for organic or unknown.
      first_ad.platformstring
      Placement platform reported by Meta (facebook, instagram…).
      first_ad.campaign_idstring
      Meta campaign id.
      first_ad.campaign_namestring
      Campaign name.
      first_ad.adset_idstring
      Meta ad set id.
      first_ad.adset_namestring
      Ad set name.
      first_ad.ad_idstring
      Meta ad id.
      first_ad.ad_namestring
      Ad name.
      owner_idstring | null
      User id of the person's owner (opiekun).
      last_activity_atstring (date-time) | null
      Last activity time.
      created_atstring (date-time)
      Creation time.
      updated_atstring (date-time)
      Last change; use with updated_since.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 409Conflicts with the current state (conflict, contact_exists, contact_merged, stage_conflict, event_conflict, source_paused, idempotency_in_progress).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/contacts/abc123" \
      -H "Authorization: Bearer $API_KEY"
    PATCH/contacts/{id}

    Update a contact

    Changes name, phone, e-mail or tags; every change is a history line.

    Scope: contacts:write

    Parameters
    NameDescription
    idpathstringRequiredRecord id.
    Request body application/json
    NameDescription
    namestring
    Full name.
    phonestring
    Phone; an empty string removes it (a phone or an e-mail must remain).
    emailstring
    E-mail; an empty string removes it.
    tagsstring[]
    Replaces all tags.
    Responses
    • 200OKResponse fields
      NameDescription
      idstring
      Contact id.
      namestring
      Full name.
      phonestring
      Phone in E.164 form, empty when unknown.
      emailstring
      E-mail, empty when unknown.
      stagestring
      Sales stage.Values: new, contacted, qualified, sold, rejected
      cycleinteger
      Sales cycle number; a new lead for a closed person opens the next cycle.
      tagsstring[]
      Tags.
      sms_optoutboolean
      True when SMS to this person are blocked.
      first_sourcestring
      Where the person first came from (form name, "Ręcznie"…).
      first_adAdSource | null
      The first ad the person came from, null for organic or unknown.
      first_ad.platformstring
      Placement platform reported by Meta (facebook, instagram…).
      first_ad.campaign_idstring
      Meta campaign id.
      first_ad.campaign_namestring
      Campaign name.
      first_ad.adset_idstring
      Meta ad set id.
      first_ad.adset_namestring
      Ad set name.
      first_ad.ad_idstring
      Meta ad id.
      first_ad.ad_namestring
      Ad name.
      owner_idstring | null
      User id of the person's owner (opiekun).
      last_activity_atstring (date-time) | null
      Last activity time.
      created_atstring (date-time)
      Creation time.
      updated_atstring (date-time)
      Last change; use with updated_since.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 409Conflicts with the current state (conflict, contact_exists, contact_merged, stage_conflict, event_conflict, source_paused, idempotency_in_progress).
    • 413Body over 64 KB (payload_too_large).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl -X PATCH "https://app.convs.io/api/v1/contacts/abc123" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "name": "Anna Nowak",
        "phone": "+48600100200",
        "email": "anna@example.com",
        "tags": [
          "vip"
        ]
      }'
    POST/contacts/{id}/stage

    Set the stage

    Moves the person and, in the same transaction, every open lead of the current cycle; leads with an active flow get a Meta conversion event queued. The same path as „Zmień etap” in the panel; stage automations start.

    Scope: contacts:write

    Parameters
    NameDescription
    idpathstringRequiredRecord id.
    Idempotency-KeyheaderstringUnique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with Idempotent-Replayed: true.Max length: 255
    Request body application/json
    NameDescription
    stagestringRequired
    Target stage.Values: new, contacted, qualified, sold, rejected
    valuenumber
    Sale value (for sold); counted once, on the newest lead whose Meta event is queued.
    currencystring
    ISO 4217 currency of value, e.g. PLN.
    Responses
    • 200OKResponse fields
      NameDescription
      contactContact
      contact.idstring
      Contact id.
      contact.namestring
      Full name.
      contact.phonestring
      Phone in E.164 form, empty when unknown.
      contact.emailstring
      E-mail, empty when unknown.
      contact.stagestring
      Sales stage.Values: new, contacted, qualified, sold, rejected
      contact.cycleinteger
      Sales cycle number; a new lead for a closed person opens the next cycle.
      contact.tagsstring[]
      Tags.
      contact.sms_optoutboolean
      True when SMS to this person are blocked.
      contact.first_sourcestring
      Where the person first came from (form name, "Ręcznie"…).
      contact.first_adAdSource | null
      The first ad the person came from, null for organic or unknown.
      contact.first_ad.platformstring
      Placement platform reported by Meta (facebook, instagram…).
      contact.first_ad.campaign_idstring
      Meta campaign id.
      contact.first_ad.campaign_namestring
      Campaign name.
      contact.first_ad.adset_idstring
      Meta ad set id.
      contact.first_ad.adset_namestring
      Ad set name.
      contact.first_ad.ad_idstring
      Meta ad id.
      contact.first_ad.ad_namestring
      Ad name.
      contact.owner_idstring | null
      User id of the person's owner (opiekun).
      contact.last_activity_atstring (date-time) | null
      Last activity time.
      contact.created_atstring (date-time)
      Creation time.
      contact.updated_atstring (date-time)
      Last change; use with updated_since.
      meta_events_queuedinteger
      Meta conversion events queued for open leads (the queue sends them; this is not a confirmation from Meta).
      leads_without_sendinteger
      Open leads moved without a Meta event (no active flow for the stage).
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 409Conflicts with the current state (conflict, contact_exists, contact_merged, stage_conflict, event_conflict, source_paused, idempotency_in_progress).
    • 413Body over 64 KB (payload_too_large).
    • 422Idempotency-Key reused with a different body (idempotency_key_reused).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl -X POST "https://app.convs.io/api/v1/contacts/abc123/stage" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
      -H "Content-Type: application/json" \
      -d '{
        "stage": "sold",
        "value": 1200,
        "currency": "PLN"
      }'
    JavaScript
    const response = await fetch('https://app.convs.io/api/v1/contacts/abc123/stage', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.API_KEY}`,
        'Idempotency-Key': '5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        "stage": "sold",
        "value": 1200,
        "currency": "PLN"
      }),
    })
    if (!response.ok) throw new Error((await response.json()).error.message)
    const data = await response.json()
    console.log(data)
    Python
    import os
    
    import requests
    
    response = requests.post(
        "https://app.convs.io/api/v1/contacts/abc123/stage",
        headers={
            "Authorization": f"Bearer {os.environ['API_KEY']}",
            "Idempotency-Key": "5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b",
        },
        json={
            "stage": "sold",
            "value": 1200,
            "currency": "PLN",
        },
        timeout=30,
    )
    response.raise_for_status()
    print(response.json())
    PHP
    <?php
    $ch = curl_init('https://app.convs.io/api/v1/contacts/abc123/stage');
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => 'POST',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . getenv('API_KEY'),
            'Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b',
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'stage' => 'sold',
            'value' => 1200,
            'currency' => 'PLN',
        ]),
    ]);
    $data = json_decode(curl_exec($ch), true);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($status >= 400) {
        throw new RuntimeException($data['error']['message']);
    }
    print_r($data);
    POST/contacts/{id}/notes

    Add a note

    Adds a note, call or meeting to the person's history.

    Scope: contacts:write

    Parameters
    NameDescription
    idpathstringRequiredRecord id.
    Idempotency-KeyheaderstringUnique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with Idempotent-Replayed: true.Max length: 255
    Request body application/json
    NameDescription
    kindstring
    Entry kind.Values: note, call, meetingDefault: note
    textstringRequired
    Text, 1–5000 characters.Max length: 5000
    timestring (date-time)
    When it happened (default: now); not in the future.
    Responses
    • 201CreatedResponse fields
      NameDescription
      idstring
      History entry id.
      contact_idstring
      Contact id.
      kindstring
      Entry kind.
      textstring
      Text.
      timestring (date-time)
      Entry time.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 409Conflicts with the current state (conflict, contact_exists, contact_merged, stage_conflict, event_conflict, source_paused, idempotency_in_progress).
    • 413Body over 64 KB (payload_too_large).
    • 422Idempotency-Key reused with a different body (idempotency_key_reused).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl -X POST "https://app.convs.io/api/v1/contacts/abc123/notes" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
      -H "Content-Type: application/json" \
      -d '{
        "kind": "call",
        "text": "Called back, wants an offer by Friday."
      }'
    POST/contacts/{id}/tags

    Add or remove tags

    Adds and removes tags without sending the whole list.

    Scope: contacts:write

    Parameters
    NameDescription
    idpathstringRequiredRecord id.
    Idempotency-KeyheaderstringUnique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with Idempotent-Replayed: true.Max length: 255
    Request body application/json
    NameDescription
    addstring[]
    Tags to add.
    removestring[]
    Tags to remove.
    Responses
    • 200OKResponse fields
      NameDescription
      idstring
      Contact id.
      namestring
      Full name.
      phonestring
      Phone in E.164 form, empty when unknown.
      emailstring
      E-mail, empty when unknown.
      stagestring
      Sales stage.Values: new, contacted, qualified, sold, rejected
      cycleinteger
      Sales cycle number; a new lead for a closed person opens the next cycle.
      tagsstring[]
      Tags.
      sms_optoutboolean
      True when SMS to this person are blocked.
      first_sourcestring
      Where the person first came from (form name, "Ręcznie"…).
      first_adAdSource | null
      The first ad the person came from, null for organic or unknown.
      first_ad.platformstring
      Placement platform reported by Meta (facebook, instagram…).
      first_ad.campaign_idstring
      Meta campaign id.
      first_ad.campaign_namestring
      Campaign name.
      first_ad.adset_idstring
      Meta ad set id.
      first_ad.adset_namestring
      Ad set name.
      first_ad.ad_idstring
      Meta ad id.
      first_ad.ad_namestring
      Ad name.
      owner_idstring | null
      User id of the person's owner (opiekun).
      last_activity_atstring (date-time) | null
      Last activity time.
      created_atstring (date-time)
      Creation time.
      updated_atstring (date-time)
      Last change; use with updated_since.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 409Conflicts with the current state (conflict, contact_exists, contact_merged, stage_conflict, event_conflict, source_paused, idempotency_in_progress).
    • 413Body over 64 KB (payload_too_large).
    • 422Idempotency-Key reused with a different body (idempotency_key_reused).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl -X POST "https://app.convs.io/api/v1/contacts/abc123/tags" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
      -H "Content-Type: application/json" \
      -d '{
        "add": [
          "vip"
        ],
        "remove": [
          "cold"
        ]
      }'
    POST/contacts/{id}/sms-optout

    Block or allow SMS

    Keeps the „nie wysyłaj” list and every person with the same phone in step.

    Scope: contacts:write

    Parameters
    NameDescription
    idpathstringRequiredRecord id.
    Idempotency-KeyheaderstringUnique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with Idempotent-Replayed: true.Max length: 255
    Request body application/json
    NameDescription
    sms_optoutbooleanRequired
    True blocks SMS to the person's phone (for every person sharing it), false allows them again.
    Responses
    • 200OKResponse fields
      NameDescription
      idstring
      Contact id.
      namestring
      Full name.
      phonestring
      Phone in E.164 form, empty when unknown.
      emailstring
      E-mail, empty when unknown.
      stagestring
      Sales stage.Values: new, contacted, qualified, sold, rejected
      cycleinteger
      Sales cycle number; a new lead for a closed person opens the next cycle.
      tagsstring[]
      Tags.
      sms_optoutboolean
      True when SMS to this person are blocked.
      first_sourcestring
      Where the person first came from (form name, "Ręcznie"…).
      first_adAdSource | null
      The first ad the person came from, null for organic or unknown.
      first_ad.platformstring
      Placement platform reported by Meta (facebook, instagram…).
      first_ad.campaign_idstring
      Meta campaign id.
      first_ad.campaign_namestring
      Campaign name.
      first_ad.adset_idstring
      Meta ad set id.
      first_ad.adset_namestring
      Ad set name.
      first_ad.ad_idstring
      Meta ad id.
      first_ad.ad_namestring
      Ad name.
      owner_idstring | null
      User id of the person's owner (opiekun).
      last_activity_atstring (date-time) | null
      Last activity time.
      created_atstring (date-time)
      Creation time.
      updated_atstring (date-time)
      Last change; use with updated_since.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 409Conflicts with the current state (conflict, contact_exists, contact_merged, stage_conflict, event_conflict, source_paused, idempotency_in_progress).
    • 413Body over 64 KB (payload_too_large).
    • 422Idempotency-Key reused with a different body (idempotency_key_reused).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl -X POST "https://app.convs.io/api/v1/contacts/abc123/sms-optout" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
      -H "Content-Type: application/json" \
      -d '{
        "sms_optout": true
      }'

    Leads

    Meta Instant Forms leads.

    GET/leads

    List leads

    Meta Instant Forms leads. answers are included only for keys with leads:answers.

    Scope: leads:read

    Parameters
    NameDescription
    limitqueryintegerPage size, 1–100 (default 25).Default: 25Range: 1–100
    cursorquerystringnext_cursor of the previous page.
    orderquerystringSort by updated_at (then id): desc newest first (default) or asc.Values: asc, descDefault: desc
    updated_sincequerystring (date-time)Only records changed at or after this ISO 8601 time.
    formquerystringConvs form (source) id.
    campaignquerystringMeta campaign id.
    adsetquerystringMeta ad set id.
    adquerystringMeta ad id.
    contactquerystringContact id.
    stagequerystringStage.Values: new, contacted, qualified, sold, rejected
    fromquerystringSubmitted on or after (YYYY-MM-DD in the organisation's zone, or ISO 8601).
    toquerystringSubmitted on or before.
    Responses
    • 200OKResponse fields
      NameDescription
      dataLead[]Required
      data[].idstring
      Lead id in Convs.
      data[].meta_lead_idstring
      Lead id in Meta.
      data[].form_idstring
      Convs source (form) id; use as form filter.
      data[].form_namestring
      Form name.
      data[].meta_form_idstring
      Meta form id.
      data[].meta_page_idstring
      Meta Page id.
      data[].stagestring
      Lead stage.Values: new, contacted, qualified, sold, rejected
      data[].cycleinteger
      Sales cycle of the person this lead belongs to.
      data[].contact_idstring | null
      The person (contact) of this lead.
      data[].fetch_statusstring
      ready when the lead data is stored.
      data[].adAdSource | null
      Ad the lead came from, null for organic or unknown.
      data[].ad.platformstring
      Placement platform reported by Meta (facebook, instagram…).
      data[].ad.campaign_idstring
      Meta campaign id.
      data[].ad.campaign_namestring
      Campaign name.
      data[].ad.adset_idstring
      Meta ad set id.
      data[].ad.adset_namestring
      Ad set name.
      data[].ad.ad_idstring
      Meta ad id.
      data[].ad.ad_namestring
      Ad name.
      data[].stagesobject[]
      Stage history.
      data[].stages[].stagestring
      data[].stages[].atstring (date-time)
      data[].stages[].meta_event_queuedboolean
      data[].answersLeadAnswer[]
      Form answers. Only for keys with leads:answers.
      data[].answers[].keystring
      Field key (also the automation variable).
      data[].answers[].labelstring
      Question as shown in the form.
      data[].answers[].valuesstring[]
      Answers.
      data[].created_atstring (date-time) | null
      When the lead was submitted.
      data[].updated_atstring (date-time)
      Last change.
      next_cursorstring | nullRequired
      Pass as cursor to get the next page; null on the last page.
      has_morebooleanRequired
      True when another page exists.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/leads" \
      -H "Authorization: Bearer $API_KEY"
    GET/leads/{id}

    Get a lead

    One lead; answers only with leads:answers.

    Scope: leads:read

    Parameters
    NameDescription
    idpathstringRequiredRecord id.
    Responses
    • 200OKResponse fields
      NameDescription
      idstring
      Lead id in Convs.
      meta_lead_idstring
      Lead id in Meta.
      form_idstring
      Convs source (form) id; use as form filter.
      form_namestring
      Form name.
      meta_form_idstring
      Meta form id.
      meta_page_idstring
      Meta Page id.
      stagestring
      Lead stage.Values: new, contacted, qualified, sold, rejected
      cycleinteger
      Sales cycle of the person this lead belongs to.
      contact_idstring | null
      The person (contact) of this lead.
      fetch_statusstring
      ready when the lead data is stored.
      adAdSource | null
      Ad the lead came from, null for organic or unknown.
      ad.platformstring
      Placement platform reported by Meta (facebook, instagram…).
      ad.campaign_idstring
      Meta campaign id.
      ad.campaign_namestring
      Campaign name.
      ad.adset_idstring
      Meta ad set id.
      ad.adset_namestring
      Ad set name.
      ad.ad_idstring
      Meta ad id.
      ad.ad_namestring
      Ad name.
      stagesobject[]
      Stage history.
      stages[].stagestring
      stages[].atstring (date-time)
      stages[].meta_event_queuedboolean
      answersLeadAnswer[]
      Form answers. Only for keys with leads:answers.
      answers[].keystring
      Field key (also the automation variable).
      answers[].labelstring
      Question as shown in the form.
      answers[].valuesstring[]
      Answers.
      created_atstring (date-time) | null
      When the lead was submitted.
      updated_atstring (date-time)
      Last change.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/leads/abc123" \
      -H "Authorization: Bearer $API_KEY"

    Conversions

    Conversions sent to Meta and their deliveries.

    A 202 from POST /events only means the conversion is queued for Meta. A delivery status of accepted means Meta acknowledged receipt. Neither proves that a campaign optimises on this data or that the conversion will be attributed to an ad.

    GET/events

    List events

    Conversions with their deliveries; payloads are never returned.

    Scope: events:read

    Parameters
    NameDescription
    limitqueryintegerPage size, 1–100 (default 25).Default: 25Range: 1–100
    cursorquerystringnext_cursor of the previous page.
    orderquerystringSort by updated_at (then id): desc newest first (default) or asc.Values: asc, descDefault: desc
    updated_sincequerystring (date-time)Only records changed at or after this ISO 8601 time.
    sourcequerystringSource id.
    statusquerystringYour status.
    external_idquerystringYour record id.
    Responses
    • 200OKResponse fields
      NameDescription
      dataEvent[]Required
      data[].idstring
      Event id.
      data[].source_idstring
      Source id.
      data[].external_idstring
      Your record id.
      data[].statusstring
      Your status.
      data[].meta_event_idstring
      Meta deduplication id.
      data[].event_timestring (date-time) | null
      Event time.
      data[].valuenumber | null
      Value.
      data[].currencystring
      Currency.
      data[].deliveriesDelivery[]
      Deliveries of this event.
      data[].deliveries[].idstring
      Delivery id.
      data[].deliveries[].event_idstring
      Convs event id.
      data[].deliveries[].target_idstring
      Meta receiver (dataset connection) id.
      data[].deliveries[].dataset_idstring
      Meta dataset (pixel) id.
      data[].deliveries[].event_namestring
      Meta event name, e.g. Lead, Purchase.
      data[].deliveries[].meta_event_idstring
      event_id sent to Meta.
      data[].deliveries[].modestring
      test or live.
      data[].deliveries[].statusstring
      Delivery status.Values: pending, sending, retry, accepted, failed, cancelled
      data[].deliveries[].attemptsinteger
      Attempts so far.
      data[].deliveries[].last_errorstring
      Last error, empty when none.
      data[].deliveries[].accepted_atstring (date-time) | null
      When Meta accepted it (receipt, not proof of attribution).
      data[].deliveries[].next_attempt_atstring (date-time) | null
      Next attempt.
      data[].deliveries[].created_atstring (date-time)
      Creation time.
      data[].deliveries[].updated_atstring (date-time)
      Last change.
      data[].created_atstring (date-time)
      Creation time.
      data[].updated_atstring (date-time)
      Last change.
      next_cursorstring | nullRequired
      Pass as cursor to get the next page; null on the last page.
      has_morebooleanRequired
      True when another page exists.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/events" \
      -H "Authorization: Bearer $API_KEY"
    POST/events

    Send a conversion

    Server-to-server conversion ingest. The source's active flows map status to Meta events and queue one delivery per dataset; dedupe rules are unchanged. Answers 202: queued, not yet sent.

    Scope: events:write

    Parameters
    NameDescription
    Idempotency-KeyheaderstringUnique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with Idempotent-Replayed: true.Max length: 255
    Request body application/json
    NameDescription
    sourcestringRequired
    Id of a Google Sheets or Shoper source of your organisation (Połączenia → source details).
    external_idstringRequired
    Your id of the record (order, lead…), max 128 characters.Max length: 128
    statusstringRequired
    Your status mapped to a Meta event by the source's flow, e.g. QUALIFIED or purchase.Max length: 80
    consentbooleanRequired
    Must be true: you confirm a legal basis for sending the data.
    event_timeinteger | string (date-time)
    Unix seconds or ISO 8601; at most 7 days old.
    event_idstring
    Meta deduplication id; generated from source + external_id + status when omitted.
    emailstring
    Customer e-mail (hashed with SHA-256 before storage).
    phonestring
    Customer phone in E.164 (hashed).
    lead_idstring
    Meta lead id, when the conversion closes a Meta lead.
    user_idstring
    Your customer id (hashed as external_id).
    fbpstring
    _fbp cookie.
    fbcstring
    _fbc cookie.
    event_source_urlstring
    Page URL of the conversion.
    tracking_idstring
    Collector tracking id (Shoper).
    client_ip_addressstring
    Customer IP.
    client_user_agentstring
    Customer user agent.
    valuenumber
    Conversion value (required for Purchase).
    currencystring
    ISO 4217 currency (required with value).
    content_idsstring[]
    Product ids.
    Responses
    • 202Accepted into the queueResponse fields
      NameDescription
      event_idstring
      Convs event id.
      deliveries_createdinteger
      New deliveries queued for Meta datasets.
      duplicatesinteger
      Deliveries skipped as duplicates.
      sending_enabledboolean
      False when the server does not send to Meta (queued only).
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 409Conflicts with the current state (conflict, contact_exists, contact_merged, stage_conflict, event_conflict, source_paused, idempotency_in_progress).
    • 413Body over 64 KB (payload_too_large).
    • 422Idempotency-Key reused with a different body (idempotency_key_reused).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl -X POST "https://app.convs.io/api/v1/events" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
      -H "Content-Type: application/json" \
      -d '{
        "source": "src_abc123",
        "external_id": "order-1001",
        "status": "purchase",
        "consent": true,
        "event_time": "2026-10-08T10:00:00Z",
        "email": "anna@example.com",
        "phone": "+48600100200",
        "value": 1200,
        "currency": "PLN"
      }'
    JavaScript
    const response = await fetch('https://app.convs.io/api/v1/events', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${process.env.API_KEY}`,
        'Idempotency-Key': '5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        "source": "src_abc123",
        "external_id": "order-1001",
        "status": "purchase",
        "consent": true,
        "event_time": "2026-10-08T10:00:00Z",
        "email": "anna@example.com",
        "phone": "+48600100200",
        "value": 1200,
        "currency": "PLN"
      }),
    })
    if (!response.ok) throw new Error((await response.json()).error.message)
    const data = await response.json()
    console.log(data)
    Python
    import os
    
    import requests
    
    response = requests.post(
        "https://app.convs.io/api/v1/events",
        headers={
            "Authorization": f"Bearer {os.environ['API_KEY']}",
            "Idempotency-Key": "5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b",
        },
        json={
            "source": "src_abc123",
            "external_id": "order-1001",
            "status": "purchase",
            "consent": True,
            "event_time": "2026-10-08T10:00:00Z",
            "email": "anna@example.com",
            "phone": "+48600100200",
            "value": 1200,
            "currency": "PLN",
        },
        timeout=30,
    )
    response.raise_for_status()
    print(response.json())
    PHP
    <?php
    $ch = curl_init('https://app.convs.io/api/v1/events');
    curl_setopt_array($ch, [
        CURLOPT_CUSTOMREQUEST => 'POST',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => [
            'Authorization: Bearer ' . getenv('API_KEY'),
            'Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b',
            'Content-Type: application/json',
        ],
        CURLOPT_POSTFIELDS => json_encode([
            'source' => 'src_abc123',
            'external_id' => 'order-1001',
            'status' => 'purchase',
            'consent' => true,
            'event_time' => '2026-10-08T10:00:00Z',
            'email' => 'anna@example.com',
            'phone' => '+48600100200',
            'value' => 1200,
            'currency' => 'PLN',
        ]),
    ]);
    $data = json_decode(curl_exec($ch), true);
    $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);
    if ($status >= 400) {
        throw new RuntimeException($data['error']['message']);
    }
    print_r($data);
    GET/events/{id}

    Get an event

    One conversion with the status of each delivery.

    Scope: events:read

    Parameters
    NameDescription
    idpathstringRequiredRecord id.
    Responses
    • 200OKResponse fields
      NameDescription
      idstring
      Event id.
      source_idstring
      Source id.
      external_idstring
      Your record id.
      statusstring
      Your status.
      meta_event_idstring
      Meta deduplication id.
      event_timestring (date-time) | null
      Event time.
      valuenumber | null
      Value.
      currencystring
      Currency.
      deliveriesDelivery[]
      Deliveries of this event.
      deliveries[].idstring
      Delivery id.
      deliveries[].event_idstring
      Convs event id.
      deliveries[].target_idstring
      Meta receiver (dataset connection) id.
      deliveries[].dataset_idstring
      Meta dataset (pixel) id.
      deliveries[].event_namestring
      Meta event name, e.g. Lead, Purchase.
      deliveries[].meta_event_idstring
      event_id sent to Meta.
      deliveries[].modestring
      test or live.
      deliveries[].statusstring
      Delivery status.Values: pending, sending, retry, accepted, failed, cancelled
      deliveries[].attemptsinteger
      Attempts so far.
      deliveries[].last_errorstring
      Last error, empty when none.
      deliveries[].accepted_atstring (date-time) | null
      When Meta accepted it (receipt, not proof of attribution).
      deliveries[].next_attempt_atstring (date-time) | null
      Next attempt.
      deliveries[].created_atstring (date-time)
      Creation time.
      deliveries[].updated_atstring (date-time)
      Last change.
      created_atstring (date-time)
      Creation time.
      updated_atstring (date-time)
      Last change.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/events/abc123" \
      -H "Authorization: Bearer $API_KEY"
    GET/deliveries

    List deliveries

    Delivery status to Meta, e.g. status=failed&updated_since=… to follow failures.

    Scope: events:read

    Parameters
    NameDescription
    limitqueryintegerPage size, 1–100 (default 25).Default: 25Range: 1–100
    cursorquerystringnext_cursor of the previous page.
    orderquerystringSort by updated_at (then id): desc newest first (default) or asc.Values: asc, descDefault: desc
    updated_sincequerystring (date-time)Only records changed at or after this ISO 8601 time.
    statusquerystringDelivery status.Values: pending, sending, retry, accepted, failed, cancelled
    eventquerystringEvent id.
    targetquerystringReceiver id.
    Responses
    • 200OKResponse fields
      NameDescription
      dataDelivery[]Required
      data[].idstring
      Delivery id.
      data[].event_idstring
      Convs event id.
      data[].target_idstring
      Meta receiver (dataset connection) id.
      data[].dataset_idstring
      Meta dataset (pixel) id.
      data[].event_namestring
      Meta event name, e.g. Lead, Purchase.
      data[].meta_event_idstring
      event_id sent to Meta.
      data[].modestring
      test or live.
      data[].statusstring
      Delivery status.Values: pending, sending, retry, accepted, failed, cancelled
      data[].attemptsinteger
      Attempts so far.
      data[].last_errorstring
      Last error, empty when none.
      data[].accepted_atstring (date-time) | null
      When Meta accepted it (receipt, not proof of attribution).
      data[].next_attempt_atstring (date-time) | null
      Next attempt.
      data[].created_atstring (date-time)
      Creation time.
      data[].updated_atstring (date-time)
      Last change.
      next_cursorstring | nullRequired
      Pass as cursor to get the next page; null on the last page.
      has_morebooleanRequired
      True when another page exists.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/deliveries" \
      -H "Authorization: Bearer $API_KEY"

    Reports

    Campaign and creative numbers.

    GET/campaigns/report

    Campaign report

    Spend, leads, qualified leads, sales and costs per campaign, ad set or ad — the numbers of the Kampanie page.

    Scope: campaigns:read

    Parameters
    NameDescription
    fromquerystringFirst day YYYY-MM-DD (default: 30 days ago).
    toquerystringLast day YYYY-MM-DD (range up to 366 days).
    levelquerystringRow level.Values: campaign, adset, adDefault: campaign
    campaignquerystringOnly this campaign (for adset/ad level).
    adsetquerystringOnly this ad set (ad level).
    Responses
    • 200OKResponse fields
      NameDescription
      fromstring
      First day (organisation time zone).
      tostring
      Last day.
      levelstring
      Row level.Values: campaign, adset, ad
      totalsobject[]
      Totals per currency: current and the previous period of the same length.
      rowsobject[]
      One row per campaign / ad set / ad with metrics (spend, meta_leads, leads, qualified, rejected, sales, revenue, cost_per_lead, cost_per_qualified, cost_per_sale, revenue_per_spend; costs are null when the divisor is 0).
      organicobject
      Leads without a known ad.
      syncsobject[]
      Spend sync state per ad account.
      maturingboolean
      True when the range includes the last 30 days (sales may still come).
      unattributedinteger
      Leads of the period with a known source but no id at this level.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/campaigns/report" \
      -H "Authorization: Bearer $API_KEY"
    GET/creatives/report

    Creative report

    Metrics per creative group with weekly momentum and the fatigue rule — the numbers of the Kreacje page.

    Scope: creatives:read

    Parameters
    NameDescription
    fromquerystringFirst day YYYY-MM-DD.
    toquerystringLast day YYYY-MM-DD.
    formatquerystringCreative format.
    campaignquerystringMeta campaign id.
    ad_accountquerystringAd account id.
    tagquerystringTag filter dimension:value (hook, creator, offer).
    weeksqueryintegerWeeks of momentum, 4–8.
    Responses
    • 200OKResponse fields
      NameDescription
      fromstring
      First day.
      tostring
      Last day.
      weeksarray[]
      Week ranges used for momentum.
      rowsobject[]
      One row per creative group (same video, image or creative) with metrics, weekly trend, fatigue verdict and tags.
      totalsobject[]
      Totals per currency.
      syncsobject[]
      Spend sync state per ad account.
      unattributedinteger
      Leads without a known creative.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/creatives/report" \
      -H "Authorization: Bearer $API_KEY"

    Background jobs

    Background work.

    GET/jobs

    List background jobs

    Lead imports, spend syncs, sheet syncs and other background work of the organisation.

    Scope: jobs:read

    Parameters
    NameDescription
    limitqueryintegerPage size, 1–100 (default 25).Default: 25Range: 1–100
    cursorquerystringnext_cursor of the previous page.
    orderquerystringSort by updated_at (then id): desc newest first (default) or asc.Values: asc, descDefault: desc
    updated_sincequerystring (date-time)Only records changed at or after this ISO 8601 time.
    statusquerystringJob status.Values: queued, running, done, failed, cancelled
    Responses
    • 200OKResponse fields
      NameDescription
      dataJob[]Required
      data[].numberinteger
      Job number within the organisation.
      data[].kindstring
      Job kind, e.g. lead_import, ad_spend, sheets_sync.
      data[].labelstring
      Description.
      data[].statusstring
      Status.Values: queued, running, done, failed, cancelled
      data[].prioritystring
      user or background.
      data[].triggerstring
      What started it.
      data[].progressobject
      done count and text.
      data[].attemptsinteger
      Attempts.
      data[].errorstring
      Error text of a failed job.
      data[].summarystring
      Result summary.
      data[].run_afterstring (date-time) | null
      Not before.
      data[].started_atstring (date-time) | null
      Start.
      data[].finished_atstring (date-time) | null
      End.
      data[].createdstring (date-time)
      Creation time.
      data[].updated_atstring (date-time)
      Last change.
      next_cursorstring | nullRequired
      Pass as cursor to get the next page; null on the last page.
      has_morebooleanRequired
      True when another page exists.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/jobs" \
      -H "Authorization: Bearer $API_KEY"
    GET/jobs/{number}

    Get a job

    One job by its number.

    Scope: jobs:read

    Parameters
    NameDescription
    numberpathintegerRequiredJob number.
    Responses
    • 200OKResponse fields
      NameDescription
      numberinteger
      Job number within the organisation.
      kindstring
      Job kind, e.g. lead_import, ad_spend, sheets_sync.
      labelstring
      Description.
      statusstring
      Status.Values: queued, running, done, failed, cancelled
      prioritystring
      user or background.
      triggerstring
      What started it.
      progressobject
      done count and text.
      attemptsinteger
      Attempts.
      errorstring
      Error text of a failed job.
      summarystring
      Result summary.
      run_afterstring (date-time) | null
      Not before.
      started_atstring (date-time) | null
      Start.
      finished_atstring (date-time) | null
      End.
      createdstring (date-time)
      Creation time.
      updated_atstring (date-time)
      Last change.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/jobs/42" \
      -H "Authorization: Bearer $API_KEY"

    Webhooks

    Events pushed to your server.

    GET/webhooks

    List webhooks

    Webhook subscriptions of the organisation (up to 10).

    Scope: webhooks:read

    Responses
    • 200OKResponse fields
      NameDescription
      dataWebhook[]Required
      data[].idstring
      Webhook id.
      data[].urlstring
      HTTPS endpoint that receives POSTs.
      data[].descriptionstring
      Your note.
      data[].eventsstring[]
      Subscribed events.Values: contact.created, contact.stage_changed, lead.created, delivery.failed
      data[].activeboolean
      False pauses deliveries.
      data[].last_statusinteger
      HTTP status of the last attempt (0 = no answer).
      data[].last_errorstring
      Last error.
      data[].last_delivery_atstring (date-time) | null
      Last attempt.
      data[].consecutive_failuresinteger
      Failed attempts in a row.
      data[].created_atstring (date-time)
      Creation time.
      data[].updated_atstring (date-time)
      Last change.
      next_cursorstring | nullRequired
      Pass as cursor to get the next page; null on the last page.
      has_morebooleanRequired
      True when another page exists.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/webhooks" \
      -H "Authorization: Bearer $API_KEY"
    POST/webhooks

    Create a webhook

    Subscribes an HTTPS URL to events. The signing secret is returned only here.

    Scope: webhooks:write

    Parameters
    NameDescription
    Idempotency-KeyheaderstringUnique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with Idempotent-Replayed: true.Max length: 255
    Request body application/json
    NameDescription
    urlstringRequired
    HTTPS URL (no credentials, no private or local addresses).Max length: 2000
    eventsstring[]Required
    Events to receive.Values: contact.created, contact.stage_changed, lead.created, delivery.failed
    descriptionstring
    Your note.Max length: 200
    Responses
    • 201CreatedResponse fields
      NameDescription
      idstring
      Webhook id.
      urlstring
      HTTPS endpoint that receives POSTs.
      descriptionstring
      Your note.
      eventsstring[]
      Subscribed events.Values: contact.created, contact.stage_changed, lead.created, delivery.failed
      activeboolean
      False pauses deliveries.
      last_statusinteger
      HTTP status of the last attempt (0 = no answer).
      last_errorstring
      Last error.
      last_delivery_atstring (date-time) | null
      Last attempt.
      consecutive_failuresinteger
      Failed attempts in a row.
      created_atstring (date-time)
      Creation time.
      updated_atstring (date-time)
      Last change.
      secretstringRequired
      Signing secret (whsec_…). Shown only in this response — store it.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 409Conflicts with the current state (conflict, contact_exists, contact_merged, stage_conflict, event_conflict, source_paused, idempotency_in_progress).
    • 413Body over 64 KB (payload_too_large).
    • 422Idempotency-Key reused with a different body (idempotency_key_reused).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl -X POST "https://app.convs.io/api/v1/webhooks" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://example.com/webhooks",
        "events": [
          "contact.created",
          "contact.stage_changed"
        ]
      }'
    GET/webhooks/{id}

    Get a webhook

    One subscription with its last result.

    Scope: webhooks:read

    Parameters
    NameDescription
    idpathstringRequiredRecord id.
    Responses
    • 200OKResponse fields
      NameDescription
      idstring
      Webhook id.
      urlstring
      HTTPS endpoint that receives POSTs.
      descriptionstring
      Your note.
      eventsstring[]
      Subscribed events.Values: contact.created, contact.stage_changed, lead.created, delivery.failed
      activeboolean
      False pauses deliveries.
      last_statusinteger
      HTTP status of the last attempt (0 = no answer).
      last_errorstring
      Last error.
      last_delivery_atstring (date-time) | null
      Last attempt.
      consecutive_failuresinteger
      Failed attempts in a row.
      created_atstring (date-time)
      Creation time.
      updated_atstring (date-time)
      Last change.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/webhooks/abc123" \
      -H "Authorization: Bearer $API_KEY"
    PATCH/webhooks/{id}

    Update a webhook

    Changes URL, events, description or pauses it; resuming resets the failure count.

    Scope: webhooks:write

    Parameters
    NameDescription
    idpathstringRequiredRecord id.
    Request body application/json
    NameDescription
    urlstring
    New URL.
    eventsstring[]
    New event list.Values: contact.created, contact.stage_changed, lead.created, delivery.failed
    descriptionstring
    Your note.
    activeboolean
    Pause or resume.
    Responses
    • 200OKResponse fields
      NameDescription
      idstring
      Webhook id.
      urlstring
      HTTPS endpoint that receives POSTs.
      descriptionstring
      Your note.
      eventsstring[]
      Subscribed events.Values: contact.created, contact.stage_changed, lead.created, delivery.failed
      activeboolean
      False pauses deliveries.
      last_statusinteger
      HTTP status of the last attempt (0 = no answer).
      last_errorstring
      Last error.
      last_delivery_atstring (date-time) | null
      Last attempt.
      consecutive_failuresinteger
      Failed attempts in a row.
      created_atstring (date-time)
      Creation time.
      updated_atstring (date-time)
      Last change.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 413Body over 64 KB (payload_too_large).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl -X PATCH "https://app.convs.io/api/v1/webhooks/abc123" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://example.com/webhooks",
        "events": [
          "contact.created",
          "contact.stage_changed"
        ],
        "active": false
      }'
    DELETE/webhooks/{id}

    Delete a webhook

    Removes the subscription and its queued deliveries.

    Scope: webhooks:write

    Parameters
    NameDescription
    idpathstringRequiredRecord id.
    Responses
    • 204Deleted
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl -X DELETE "https://app.convs.io/api/v1/webhooks/abc123" \
      -H "Authorization: Bearer $API_KEY"
    POST/webhooks/{id}/test

    Send a test ping

    Queues a signed ping event to check your endpoint and signature verification.

    Scope: webhooks:write

    Parameters
    NameDescription
    idpathstringRequiredRecord id.
    Idempotency-KeyheaderstringUnique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with Idempotent-Replayed: true.Max length: 255
    Responses
    • 202QueuedResponse fields
      NameDescription
      delivery_idstring
      Queued delivery of a ping event.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 409Conflicts with the current state (conflict, contact_exists, contact_merged, stage_conflict, event_conflict, source_paused, idempotency_in_progress).
    • 422Idempotency-Key reused with a different body (idempotency_key_reused).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl -X POST "https://app.convs.io/api/v1/webhooks/abc123/test" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b"
    POST/webhooks/{id}/rotate-secret

    Rotate the signing secret

    Replaces the signing secret at once; the new secret is returned only here. Deliveries still queued are signed with the new secret.

    Scope: webhooks:write

    Parameters
    NameDescription
    idpathstringRequiredRecord id.
    Idempotency-KeyheaderstringUnique key (1–255 visible ASCII characters). Repeating a POST with the same key and body within 24 hours returns the stored response with Idempotent-Replayed: true.Max length: 255
    Responses
    • 200OKResponse fields
      NameDescription
      idstring
      Webhook id.
      urlstring
      HTTPS endpoint that receives POSTs.
      descriptionstring
      Your note.
      eventsstring[]
      Subscribed events.Values: contact.created, contact.stage_changed, lead.created, delivery.failed
      activeboolean
      False pauses deliveries.
      last_statusinteger
      HTTP status of the last attempt (0 = no answer).
      last_errorstring
      Last error.
      last_delivery_atstring (date-time) | null
      Last attempt.
      consecutive_failuresinteger
      Failed attempts in a row.
      created_atstring (date-time)
      Creation time.
      updated_atstring (date-time)
      Last change.
      secretstringRequired
      Signing secret (whsec_…). Shown only in this response — store it.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 409Conflicts with the current state (conflict, contact_exists, contact_merged, stage_conflict, event_conflict, source_paused, idempotency_in_progress).
    • 422Idempotency-Key reused with a different body (idempotency_key_reused).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl -X POST "https://app.convs.io/api/v1/webhooks/abc123/rotate-secret" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b"
    GET/webhooks/{id}/deliveries

    List webhook deliveries

    Recent attempts (kept 30 days), newest first.

    Scope: webhooks:read

    Parameters
    NameDescription
    idpathstringRequiredRecord id.
    limitqueryintegerPage size, 1–100 (default 25).Default: 25Range: 1–100
    cursorquerystringnext_cursor of the previous page.
    orderquerystringSort by updated_at (then id): desc newest first (default) or asc.Values: asc, descDefault: desc
    updated_sincequerystring (date-time)Only records changed at or after this ISO 8601 time.
    Responses
    • 200OKResponse fields
      NameDescription
      dataWebhookDelivery[]Required
      data[].idstring
      Delivery id (header X-Convs-Delivery).
      data[].webhook_idstring
      Webhook id.
      data[].eventstring
      Event type.
      data[].event_idstring
      Event id (evt_…, the same for every webhook of one event).
      data[].statusstring
      Status.Values: pending, sending, retry, delivered, failed, cancelled
      data[].attemptsinteger
      Attempts.
      data[].response_statusinteger
      HTTP status of the last attempt.
      data[].last_errorstring
      Last error.
      data[].next_attempt_atstring (date-time) | null
      Next attempt.
      data[].delivered_atstring (date-time) | null
      Delivered at.
      data[].created_atstring (date-time)
      Creation time.
      data[].updated_atstring (date-time)
      Last change.
      next_cursorstring | nullRequired
      Pass as cursor to get the next page; null on the last page.
      has_morebooleanRequired
      True when another page exists.
    • 400Invalid input (validation_error, invalid_json, invalid_cursor).
    • 401Missing, unknown, revoked or expired key (unauthorized, key_revoked, key_expired).
    • 403The key lacks the scope (insufficient_scope, details.required_scope).
    • 404No such record in your organisation (not_found).
    • 429Over 120 requests per minute for this key (rate_limited); wait Retry-After seconds.
    Example
    curl
    curl "https://app.convs.io/api/v1/webhooks/abc123/deliveries" \
      -H "Authorization: Bearer $API_KEY"

    Webhooks

    A webhook sends a JSON POST to your HTTPS URL when something changes in the organisation. An organisation can have up to 10 webhooks. The signing secret (whsec_…) is returned only in the response that creates the webhook.

    Delivery is at least once: the same event can arrive more than once, so skip any id you have already handled. Payloads carry ids and states only, never names, phones or e-mails; fetch details with GET.

    Events

    contact.created

    A person was added (form lead, manual, API, automation).

    Example payload · JSON
    {
      "id": "evt_9f2c4b1a7d3e5f60a1b2c3d4",
      "type": "contact.created",
      "created_at": "2026-10-08T10:00:00Z",
      "organization_id": "org123",
      "data": {
        "contact_id": "abc123",
        "stage": "new",
        "first_source": "Formularz kontaktowy"
      }
    }

    contact.stage_changed

    A person moved to another stage.

    Example payload · JSON
    {
      "id": "evt_9f2c4b1a7d3e5f60a1b2c3d4",
      "type": "contact.stage_changed",
      "created_at": "2026-10-08T10:00:00Z",
      "organization_id": "org123",
      "data": {
        "contact_id": "abc123",
        "from": "new",
        "to": "qualified",
        "cycle": 1,
        "meta_events_queued": 1,
        "leads_without_send": 0
      }
    }

    lead.created

    A Meta Instant Forms lead was stored.

    Example payload · JSON
    {
      "id": "evt_9f2c4b1a7d3e5f60a1b2c3d4",
      "type": "lead.created",
      "created_at": "2026-10-08T10:00:00Z",
      "organization_id": "org123",
      "data": {
        "lead_id": "l123",
        "meta_lead_id": "1234567890",
        "form_id": "src123",
        "contact_id": "abc123",
        "campaign_id": "120200000",
        "adset_id": "",
        "ad_id": "",
        "created_at": "2026-10-08T10:00:00Z"
      }
    }

    delivery.failed

    A conversion delivery to Meta failed for good.

    Example payload · JSON
    {
      "id": "evt_9f2c4b1a7d3e5f60a1b2c3d4",
      "type": "delivery.failed",
      "created_at": "2026-10-08T10:00:00Z",
      "organization_id": "org123",
      "data": {
        "delivery_id": "d123",
        "event_id": "e123",
        "target_id": "t123",
        "dataset_id": "123456789",
        "event_name": "Lead",
        "mode": "live",
        "error": "Połącz konto Meta ponownie lub sprawdź token."
      }
    }

    Headers

    HeaderDescription
    X-Convs-Signaturesha256= + hex HMAC-SHA256 of {X-Convs-Timestamp}.{raw body} with the webhook secret.
    X-Convs-TimestampUnix seconds of the attempt; reject values older than 5 minutes.
    X-Convs-EventEvent type.
    X-Convs-DeliveryDelivery id (changes per webhook, stays across retries).

    Acknowledgement and retries

    • Answer with any 2xx code within 15 seconds. Store the event first and do longer work later.
    • Any other answer, or none, is retried: up to 8 attempts in total, after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h and 24 h.
    • X-Convs-Delivery stays the same across attempts of one delivery; GET /webhooks/{id}/deliveries shows the attempt history.

    Verifying the signature

    1. Read the raw request body before anything parses it.
    2. Reject the request when the timestamp is older than 5 minutes.
    3. Compute HMAC-SHA256 of {timestamp}.{raw body} with the secret and compare it with the signature header (sha256=<hex>) in constant time.
    Node.js
    import crypto from 'node:crypto'
    import http from 'node:http'
    
    const secret = process.env.WEBHOOK_SECRET // whsec_…
    
    function verify(rawBody, timestamp, signature) {
      const age = Math.abs(Date.now() / 1000 - Number(timestamp))
      if (!timestamp || !(age <= 300)) return false
      const expected = 'sha256=' + crypto.createHmac('sha256', secret)
        .update(`${timestamp}.`)
        .update(rawBody)
        .digest('hex')
      const a = Buffer.from(String(signature ?? ''))
      const b = Buffer.from(expected)
      return a.length === b.length && crypto.timingSafeEqual(a, b)
    }
    
    http.createServer((req, res) => {
      const chunks = []
      req.on('data', chunk => chunks.push(chunk))
      req.on('end', () => {
        const raw = Buffer.concat(chunks) // raw body, before JSON parsing
        if (!verify(raw, req.headers['x-convs-timestamp'], req.headers['x-convs-signature'])) {
          res.writeHead(401).end()
          return
        }
        const event = JSON.parse(raw.toString('utf8'))
        // skip an event.id you have already handled, and answer fast
        res.writeHead(200).end()
      })
    }).listen(3000)
    Python
    import hashlib
    import hmac
    import os
    import time
    
    from flask import Flask, abort, request
    
    app = Flask(__name__)
    SECRET = os.environ["WEBHOOK_SECRET"].encode()  # whsec_…
    
    
    @app.post("/webhooks")
    def webhook():
        raw = request.get_data()  # raw body, before JSON parsing
        timestamp = request.headers.get("X-Convs-Timestamp", "")
        signature = request.headers.get("X-Convs-Signature", "")
        if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300:
            abort(401)
        expected = "sha256=" + hmac.new(SECRET, timestamp.encode() + b"." + raw, hashlib.sha256).hexdigest()
        if not hmac.compare_digest(expected, signature):
            abort(401)
        event = request.get_json()
        # skip an event.id you have already handled, and answer fast
        return "", 200
    PHP
    <?php
    $secret = getenv('WEBHOOK_SECRET'); // whsec_…
    $raw = file_get_contents('php://input'); // raw body, before JSON parsing
    $timestamp = $_SERVER['HTTP_X_CONVS_TIMESTAMP'] ?? '';
    $signature = $_SERVER['HTTP_X_CONVS_SIGNATURE'] ?? '';
    
    if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) {
        http_response_code(401);
        exit;
    }
    $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $raw, $secret);
    if (!hash_equals($expected, $signature)) {
        http_response_code(401);
        exit;
    }
    $event = json_decode($raw, true);
    // skip an event.id you have already handled, and answer fast
    http_response_code(200);

    Creating and testing

    Create a webhook, store the secret from the response, then send a test ping to check your endpoint and signature verification.

    curl
    curl -X POST "https://app.convs.io/api/v1/webhooks" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://example.com/webhooks",
        "events": [
          "contact.created",
          "contact.stage_changed"
        ]
      }'
    curl
    curl -X POST "https://app.convs.io/api/v1/webhooks/abc123/test" \
      -H "Authorization: Bearer $API_KEY" \
      -H "Idempotency-Key: 5f0c7a2e-8d41-4b6a-9e3f-1c2d3e4f5a6b"

    Changelog

    • 1.0.0 · : First public version.