agentlyleads docs
Deals API

POST /api/v1/deals

Create or update up to 1,000 deals in a single request. Each row is matched and upserted by externalId; rows with no match are created.

Request

POST /api/v1/deals HTTP/1.1
Host: agentlyleads.com
Authorization: Bearer alk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Content-Type: application/json

{
  "deals": [
    {
      "name": "Acme Corp — Annual Contract",
      "value": 12000,
      "stage": "QUALIFIED",
      "expectedCloseDate": "2026-09-30",
      "externalId": "hs_deal_789"
    },
    {
      "name": "Big Order",
      "value": 500,
      "stage": "NEW",
      "contactExternalId": "hs_contact_123"
    }
  ]
}

The deals array must contain at least 1 item and at most 1,000.

Response — 200 (all rows succeeded)

When every row in the batch succeeds, the response status is 200:

{
  "created": 1,
  "updated": 1,
  "errors": 0,
  "results": [
    { "index": 0, "status": "created", "id": "cmq1abc2def3ghi4jkl5" },
    { "index": 1, "status": "updated", "id": "cmq6mno7pqr8stu9vwx0" }
  ]
}

Each entry in results corresponds to the deal at that position in your request array (zero-indexed).

Response — 207 Multi-Status (partial success)

When at least one row fails validation, the response status is 207. Failed rows appear in results with "status": "error":

{
  "created": 1,
  "updated": 0,
  "errors": 1,
  "results": [
    { "index": 0, "status": "created", "id": "cmq1abc2def3ghi4jkl5" },
    {
      "index": 1,
      "status": "error",
      "error": "name: Required"
    }
  ]
}

A row that fails validation does not abort the batch. Valid rows are written; failed rows are skipped and reported per-index in results.

Merge on update

When a row matches an existing deal (by externalId), only the fields present in your request body are updated. Fields you omit retain their current values — this is a merge update, not a full replace.

For example, sending only { "externalId": "hs_deal_789", "value": 15000 } updates the value without touching name, stage, notes, or any other field.

Linking contacts, companies, and offerings

A deal can be linked to a contact, company, and/or offering either by their agentlyleads record ID (contactId/companyId/offeringId) or by an external system's ID (contactExternalId/companyExternalId/offeringExternalId), which the server resolves against that record's own externalId:

{
  "deals": [
    {
      "name": "Big Order",
      "value": 500,
      "contactExternalId": "hs_contact_123",
      "companyExternalId": "hs_company_456"
    }
  ]
}

If an *ExternalId is supplied but no matching record is found in the workspace, that row fails — it does not create the deal unlinked.

Attributing a deal to a campaign

Pass campaignId (the agentlyleads campaign ID) to record which campaign the deal came out of; the campaign page then rolls the deal's value up under that campaign and its offering. Like the other links it is a merge field: omit it to leave the current campaign alone, send null to unlink. A campaign from another workspace fails the row with Campaign not found..

{
  "deals": [
    { "name": "Big Order", "value": 500, "externalId": "SHOP-1", "campaignId": "cmq3camp0alk34567890" }
  ]
}

Moving a deal to a pipeline stage

stage only knows the six built-in values. If your workspace has its own pipelines, send stageId instead: the id of a stage from GET /api/v1/pipelines.

{
  "deals": [
    { "name": "Big Order", "externalId": "SHOP-1", "stageId": "cmq4stage0alk3456789" }
  ]
}

A deal sent with stageId is moved onto that stage and its pipeline, the same way as dragging it on the board. The move is logged on the deal and runs any automations that listen for a stage change. It works on create and on update.

Send stage or stageId in a row, not both; a row with both fails with Send stage or stageId, not both.. A stage id that is not in your workspace fails the row too, and the deal is not written.

Quickstart

export AL_KEY="alk_live_xxx"

# Push your deals — run this on a schedule to keep them in sync
curl -s -X POST https://agentlyleads.com/api/v1/deals \
  -H "Authorization: Bearer $AL_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "deals": [
      {
        "name": "Acme Corp — Annual Contract",
        "value": 12000,
        "stage": "QUALIFIED",
        "externalId": "hs_deal_789"
      }
    ]
  }'

A typical integration is a nightly job in your CRM or e-commerce backend that reads its deal pipeline and POSTs it here. The first run creates everything; every later run updates by externalId.

On this page