{
  "openapi": "3.1.0",
  "info": {
    "title": "EnrichLoops API",
    "version": "0.1.0",
    "description": "Send us a domain or a LinkedIn URL. We send back the company. Send us a\nLinkedIn profile URL or a work email. We send back the person. Results\ncome to you directly, or to a webhook you register (a **Loop**).\n\n## How it works\n\n1. You send an input: a domain, a LinkedIn URL, or a work email.\n2. We check what we already hold. If we have a fresh record, the task\n   settles inside the same request.\n3. Otherwise we go and get it, store the result, and queue it for you.\n4. You collect the result by polling the task, or by having it delivered\n   to your Loop's webhook.\n\nEvery enrichment endpoint (`POST /v1/companies/enrich`, `POST\n/v1/people/enrich`, `POST /v1/loops/{id}/run`) responds with a task only.\nNever the enrichment data inline, even when the task settles instantly.\n`200` means it already settled and `202` means it was queued. Either way,\nfetch the result from `GET /v1/tasks/{id}` or receive it on your Loop's\nwebhook.\n",
    "contact": {
      "name": "EnrichLoops support"
    }
  },
  "servers": [
    {
      "url": "https://api.enrichloops.com",
      "description": "Production"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Companies",
      "description": "Enqueue a company enrichment."
    },
    {
      "name": "People",
      "description": "Enqueue a person enrichment, by LinkedIn profile URL or by work email address."
    },
    {
      "name": "Tasks",
      "description": "Poll or list the async tasks behind an enrichment."
    },
    {
      "name": "Loops",
      "description": "Webhook delivery. Send records to a Loop you created in the dashboard."
    }
  ],
  "paths": {
    "/v1/companies/enrich": {
      "post": {
        "operationId": "enrichCompany",
        "tags": [
          "Companies"
        ],
        "summary": "Enrich a company",
        "description": "Enqueues an enrichment for a company, identified by domain or LinkedIn\ncompany URL. Provide exactly one of `domain` or `linkedinUrl`.\n\nResponds `200` if the task settled inside the request (a company we\nalready hold, the common case once a company has been seen), or `202`\nif it was queued. Both responses share the same shape. The body never\ncarries the enrichment result, only the task. Poll `GET /v1/tasks/{id}`\nor supply `loop_id` to get the result delivered to a Loop's webhook.\n\n**Freshness.** A stored company older than 30 days is not served as-is.\nThe request is queued (`202`) and the company is re-fetched from source\nbefore the task settles. A company inside the 30-day window is served\nfrom storage in the same request. Refreshing costs the same as any\nother request.\n\nIf a refresh cannot complete (a source is unavailable, or the company's\nLinkedIn page has been removed), the task still succeeds and returns\nthe stored record. You are never left without data you would otherwise\nhave received.\n",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Reusing the same key returns the original task instead of\ncreating and charging a new one. Use it to retry a request whose\nresponse you never received.\n\nA key is bound to the body it was first sent with. Reusing it with\na different body is rejected with `422`. Maximum 255 characters.\n",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string",
                    "description": "A company's domain, e.g. `\"stripe.com\"`.",
                    "example": "stripe.com"
                  },
                  "linkedinUrl": {
                    "type": "string",
                    "description": "A LinkedIn company page URL, as an alternative to `domain`.",
                    "example": "https://www.linkedin.com/company/stripe"
                  },
                  "loop_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Deliver the result to this Loop's webhook once it's\nready, in addition to polling this task directly.\n"
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The task settled inside the request (a record we already hold, or an idempotent replay).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskEnqueueResponse"
                }
              }
            }
          },
          "202": {
            "description": "The task was queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskEnqueueResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body (e.g. neither `domain` nor `linkedinUrl`, or both, provided).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/people/enrich": {
      "post": {
        "operationId": "enrichPerson",
        "tags": [
          "People"
        ],
        "summary": "Enrich a person",
        "description": "Enqueues an enrichment for a person, identified by LinkedIn profile\nURL or by work email address. Provide exactly one of `linkedinUrl` or\n`email`.\n\nResponds `200` if the task settled inside the request (a person we\nalready hold), or `202` if it was queued. Both responses share the same\nshape. The body never carries the result, only the task. Poll `GET\n/v1/tasks/{id}` or supply `loop_id` to get the result delivered to a\nLoop's webhook.\n\n**The two inputs take different routes.** A profile URL is an address\nwe can fetch. If we do not already hold that person, the task queues,\npulls their profile, and returns it.\n\nAn email address is resolved live (reverse email lookup). We identify\nthe company behind the domain, work out which names the local part\ncould describe, search that company's staff on LinkedIn, pull each\nplausible profile, and confirm the person holds a current position\nthere under a name the address fits. Only a confirmed person is\nreturned. The result carries an `email_match` object: `method` is\n`\"exact\"` for an address already on record, `\"resolved\"` for one\nconfirmed against a profile pulled for this request, `\"inferred\"` for\none recognized among people we already held.\n\n**When we are not sure, we say so and do not charge.** If the address\nfits more than one current employee and nothing separates them, the\ntask fails with `ambiguous`. `error` carries `candidates` (how many)\nand `hintsAccepted`. Retry the same `email` with one of `fullName`,\n`title` or `location` and we break the tie against the profiles we\nalready gathered. A miss is `not_found`. Both are refunded. An address\nwe cannot confidently resolve never costs you a credit.\n\nPersonal addresses (gmail, outlook, ...) and role addresses (info@,\nsales@) are refused as `not_found` without any lookup.\n\nAn email lookup costs 3 credits. A URL lookup costs 1. Resolving an\naddress can take several LinkedIn retrievals.\n\n**Freshness.** A stored profile older than 30 days is not served as-is.\nThe request is queued (`202`) and the profile is re-fetched before the\ntask settles. This matters more for people than for companies. A\nperson who changed jobs is not just out of date, and the new role is\nusually the reason you asked. If the refresh cannot complete, the task\nstill succeeds and returns the stored record. Compare\n`last_enriched_at` to tell the two apart. Email lookups follow the same\nrule: an address that resolves to a person we already hold is\nrefreshed the same way.\n",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "Reusing the same key returns the original task instead of\ncreating and charging a new one. Use it to retry a request whose\nresponse you never received.\n\nA key is bound to the body it was first sent with. Reusing it with\na different body is rejected with `422`. Maximum 255 characters.\n",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "linkedinUrl": {
                    "type": "string",
                    "description": "A LinkedIn profile URL, e.g. `\"https://www.linkedin.com/in/jane-doe\"`.",
                    "example": "https://www.linkedin.com/in/jane-doe"
                  },
                  "email": {
                    "type": "string",
                    "description": "A work email address, as an alternative to `linkedinUrl`.\nResolved live against LinkedIn. See the endpoint\ndescription for what happens on a miss or a tie.\n",
                    "example": "jane.doe@acme.com"
                  },
                  "fullName": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Optional hint for `email` lookups only. Read solely to\nbreak a tie after an `ambiguous` result. Never used as a\nsearch filter. Rejected alongside `linkedinUrl`.\n",
                    "example": "Jane Marie Doe"
                  },
                  "title": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Optional tie-break hint for `email` lookups. The person's job title.",
                    "example": "Head of Finance"
                  },
                  "location": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Optional tie-break hint for `email` lookups. A city or country.",
                    "example": "Amsterdam"
                  },
                  "loop_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Deliver the result to this Loop's webhook once it's\nready, in addition to polling this task directly.\n"
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The task settled inside the request (a record we already hold, or an idempotent replay).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskEnqueueResponse"
                }
              }
            }
          },
          "202": {
            "description": "The task was queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskEnqueueResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body (e.g. neither `linkedinUrl` nor `email`, or both, provided).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/v1/tasks": {
      "get": {
        "operationId": "listTasks",
        "tags": [
          "Tasks"
        ],
        "summary": "List tasks",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "$ref": "#/components/schemas/TaskStatus"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "description": "Zero-indexed page number.",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "default": 0
            }
          },
          {
            "name": "pageSize",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 25
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A page of tasks belonging to the caller's organization.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Task"
                      }
                    },
                    "total": {
                      "type": "integer"
                    }
                  },
                  "required": [
                    "data",
                    "total"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid `status` value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/v1/tasks/{id}": {
      "get": {
        "operationId": "getTask",
        "tags": [
          "Tasks"
        ],
        "summary": "Check on a task",
        "description": "Returns the task, plus `data` once it has succeeded or `error` once\nit has failed. A task belonging to another organization returns\n`404`, indistinguishable from one that doesn't exist.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The task. While `status` is `queued` or `running`, a\n`Retry-After: 2` header is included as a polling hint.\n",
            "headers": {
              "Retry-After": {
                "description": "Seconds to wait before polling again. Only present while the task is pending.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskDetailResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No task with this id in the caller's organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/v1/loops": {
      "get": {
        "operationId": "listLoops",
        "x-hidden": true,
        "tags": [
          "Loops"
        ],
        "summary": "List Loops",
        "responses": {
          "200": {
            "description": "The organization's Loops.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Loop"
                      }
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "operationId": "createLoop",
        "x-hidden": true,
        "tags": [
          "Loops"
        ],
        "summary": "Create a Loop",
        "description": "Your webhook URL must be `https` and publicly reachable (loopback and\nprivate addresses are rejected). On success you get back the Loop\nalong with a **signing secret**, shown once at creation and never\nretrievable again. If you lose it, or need to replace it, use\n`POST /v1/loops/{id}/rotate-secret`.\n",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "slug": {
                    "type": "string",
                    "description": "URL-safe identifier, unique per organization. Derived\nfrom `name` if omitted. Lowercase letters, numbers and\nsingle hyphens, 2–64 characters.\n"
                  },
                  "object_type": {
                    "type": "string",
                    "enum": [
                      "company",
                      "person"
                    ],
                    "default": "company"
                  },
                  "operation": {
                    "type": "string",
                    "enum": [
                      "enrich"
                    ],
                    "default": "enrich"
                  },
                  "webhook_url": {
                    "type": "string",
                    "format": "uri"
                  }
                },
                "required": [
                  "name",
                  "webhook_url"
                ],
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The created Loop, including its signing secret.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/LoopWithSecret"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, or a webhook URL that fails the safety check.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "description": "A Loop with this slug already exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/v1/loops/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "getLoop",
        "x-hidden": true,
        "tags": [
          "Loops"
        ],
        "summary": "Get a Loop",
        "responses": {
          "200": {
            "description": "The Loop.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Loop"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No Loop with this id in the caller's organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "operationId": "updateLoop",
        "x-hidden": true,
        "tags": [
          "Loops"
        ],
        "summary": "Update a Loop",
        "description": "Update `name`, `webhook_url`, or `enabled`. Omitted fields are left unchanged.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "enabled": {
                    "type": "boolean"
                  },
                  "webhook_url": {
                    "type": "string",
                    "format": "uri"
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated Loop.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/Loop"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Malformed body, or a webhook URL that fails the safety check.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No Loop with this id in the caller's organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "operationId": "deleteLoop",
        "x-hidden": true,
        "tags": [
          "Loops"
        ],
        "summary": "Delete a Loop",
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No Loop with this id in the caller's organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/v1/loops/{id}/rotate-secret": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "operationId": "rotateLoopSecret",
        "x-hidden": true,
        "tags": [
          "Loops"
        ],
        "summary": "Rotate a Loop's signing secret",
        "description": "Issues a new signing secret and returns it once, exactly like Loop\ncreation does. The response is the only place you will ever see it.\n\nThe previous secret stops verifying **immediately**. There is no\noverlap period. Update your receiver in the same window, or deliveries\nsigned with the new secret will fail your signature check and be\nretried on the usual schedule until they succeed or run out of\nattempts.\n",
        "responses": {
          "200": {
            "description": "The Loop, including its new signing secret.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "$ref": "#/components/schemas/LoopWithSecret"
                    }
                  },
                  "required": [
                    "data"
                  ]
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No Loop with this id in the caller's organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "The Loop has no destination to rotate a secret for.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/v1/loops/{id}/run": {
      "post": {
        "operationId": "runLoop",
        "tags": [
          "Loops"
        ],
        "summary": "Send a record to a Loop",
        "description": "Behaves like the matching direct endpoint (`POST\n/v1/companies/enrich` for a `company` Loop, `POST /v1/people/enrich`\nfor a `person` Loop). Same task response, same `200` or `202`\nsemantics, still pollable. Two differences: the object type comes from\nthe Loop's configuration rather than the URL, and once the task\nsettles, the result is also `POST`ed to the Loop's webhook.\n\nThe body is the input the matching direct endpoint takes. A `company`\nLoop accepts `domain` or `linkedinUrl`. A `person` Loop accepts\n`linkedinUrl` or `email`, plus the optional email tie-break hints.\n",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "description": "As on the direct endpoints: reusing a key replays the original\ntask, and reusing one with a different body is rejected with\n`422`.\n",
            "schema": {
              "type": "string",
              "maxLength": 255
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "domain": {
                    "type": "string",
                    "description": "A company domain. `company` Loops only.",
                    "example": "stripe.com"
                  },
                  "linkedinUrl": {
                    "type": "string",
                    "description": "A LinkedIn company page URL for a `company` Loop, or a\nLinkedIn profile URL for a `person` Loop.\n",
                    "example": "https://www.linkedin.com/company/stripe"
                  },
                  "email": {
                    "type": "string",
                    "description": "A work email address. `person` Loops only.",
                    "example": "jane.doe@acme.com"
                  },
                  "fullName": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Optional tie-break hint for `email` lookups. See `POST /v1/people/enrich`."
                  },
                  "title": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Optional tie-break hint for `email` lookups."
                  },
                  "location": {
                    "type": "string",
                    "maxLength": 200,
                    "description": "Optional tie-break hint for `email` lookups."
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The task settled inside the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskEnqueueResponse"
                }
              }
            }
          },
          "202": {
            "description": "The task was queued.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TaskEnqueueResponse"
                }
              }
            }
          },
          "400": {
            "description": "Malformed request body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "description": "No Loop with this id in the caller's organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "409": {
            "description": "The Loop is disabled, or has no enabled destination.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/IdempotencyKeyReused"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Pass your API key as a bearer token: `Authorization: Bearer\nYOUR_API_KEY`. Generate and manage keys from your dashboard under\n**Settings → API Keys**.\n"
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "content": {
          "text/plain": {
            "schema": {
              "type": "string",
              "example": "Unauthorized"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "Insufficient or expired credits.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                },
                "code": {
                  "type": "string",
                  "enum": [
                    "credits_expired",
                    "insufficient_credits"
                  ]
                },
                "creditsRemaining": {
                  "type": "number"
                }
              },
              "required": [
                "error"
              ]
            }
          }
        }
      },
      "IdempotencyKeyReused": {
        "description": "This `Idempotency-Key` was already used with a different request\nbody. A key identifies one request, so replaying it against different\ninput would return a task for the wrong target. Nothing was charged\nand no task was created. Retry with a new key.\n",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                },
                "code": {
                  "type": "string",
                  "enum": [
                    "idempotency_key_reused"
                  ]
                }
              },
              "required": [
                "error"
              ]
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limited. `Retry-After` gives the number of seconds to wait.\n\nTwo limits apply: one per client address and one per API key. A\n`code: \"queue_full\"` body means neither was exceeded. Instead the\norganization's queued-task backlog is full, and the fix is to let\nin-flight tasks drain rather than to slow the request rate.\n",
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                },
                "code": {
                  "type": "string",
                  "enum": [
                    "queue_full"
                  ]
                },
                "queuedTasks": {
                  "type": "integer"
                }
              },
              "required": [
                "error"
              ]
            }
          }
        }
      },
      "InternalError": {
        "description": "Something went wrong on our end.",
        "content": {
          "application/json": {
            "schema": {
              "type": "object",
              "properties": {
                "error": {
                  "type": "string"
                },
                "requestId": {
                  "type": "string"
                }
              },
              "required": [
                "error"
              ]
            }
          }
        }
      }
    },
    "schemas": {
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "error": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "TaskStatus": {
        "type": "string",
        "enum": [
          "queued",
          "running",
          "succeeded",
          "failed",
          "canceled"
        ]
      },
      "TaskErrorCode": {
        "type": "string",
        "description": "The same codes appear whether you poll the task or receive it on a webhook. Safe to branch on.",
        "enum": [
          "not_found",
          "ambiguous",
          "provider_unavailable",
          "timed_out",
          "invalid_input",
          "internal_error"
        ]
      },
      "TaskError": {
        "type": "object",
        "properties": {
          "code": {
            "$ref": "#/components/schemas/TaskErrorCode"
          },
          "message": {
            "type": "string"
          },
          "candidates": {
            "type": "integer",
            "description": "Present when `code` is `ambiguous`: how many current employees\nthe address fit. Names are never included.\n"
          },
          "hintsAccepted": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present when `code` is `ambiguous`: the request fields that\nwould break the tie on a retry (`fullName`, `title`, `location`).\n"
          }
        },
        "required": [
          "code",
          "message"
        ]
      },
      "Task": {
        "type": "object",
        "description": "`input` and `target` are dropped in future revisions to a single\nfield; today `target` is the normalized domain or LinkedIn URL the\ntask resolved to.\n",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "object_type": {
            "type": "string",
            "enum": [
              "company",
              "person"
            ]
          },
          "operation": {
            "type": "string",
            "enum": [
              "enrich"
            ]
          },
          "status": {
            "$ref": "#/components/schemas/TaskStatus"
          },
          "input": {
            "type": "object",
            "additionalProperties": true
          },
          "target": {
            "type": "string",
            "description": "The normalized input we resolved. The domain, LinkedIn URL or email you sent."
          },
          "credits_charged": {
            "type": "integer"
          },
          "loop_id": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "idempotency_key": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "queued_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "started_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "finished_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "expires_at": {
            "type": "string",
            "format": "date-time",
            "description": "If the task is still running past this time, it will be marked `failed`."
          }
        },
        "required": [
          "id",
          "object_type",
          "operation",
          "status",
          "target",
          "credits_charged",
          "created_at"
        ]
      },
      "TaskEnqueueResponse": {
        "type": "object",
        "description": "Never carries `data`. Carries `error` only if the task already failed\ninside the request. Fetch the result from `GET /v1/tasks/{id}` or a\nLoop webhook.\n",
        "properties": {
          "task": {
            "$ref": "#/components/schemas/Task"
          },
          "error": {
            "$ref": "#/components/schemas/TaskError"
          }
        },
        "required": [
          "task"
        ]
      },
      "TaskDetailResponse": {
        "type": "object",
        "properties": {
          "task": {
            "$ref": "#/components/schemas/Task"
          },
          "data": {
            "description": "The enriched object. Which shape it takes follows the task's\n`object_type`: a `Company` for `company`, a `Person` for `person`.\n",
            "oneOf": [
              {
                "$ref": "#/components/schemas/Company"
              },
              {
                "$ref": "#/components/schemas/Person"
              }
            ]
          },
          "error": {
            "$ref": "#/components/schemas/TaskError"
          }
        },
        "required": [
          "task"
        ]
      },
      "Headquarters": {
        "type": [
          "object",
          "null"
        ],
        "additionalProperties": true,
        "properties": {
          "city": {
            "type": "string"
          },
          "country": {
            "type": "string"
          }
        }
      },
      "Company": {
        "type": "object",
        "description": "Present as `data` once a task's `status` is `\"succeeded\"`.",
        "properties": {
          "name": {
            "type": "string"
          },
          "domain": {
            "type": [
              "string",
              "null"
            ]
          },
          "linkedin_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "linkedin_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "linkedin_followers": {
            "type": [
              "integer",
              "null"
            ]
          },
          "employee_count": {
            "type": [
              "integer",
              "null"
            ]
          },
          "employee_range": {
            "type": [
              "string",
              "null"
            ]
          },
          "primary_industry": {
            "type": [
              "string",
              "null"
            ]
          },
          "organization_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "year_founded": {
            "type": [
              "integer",
              "null"
            ]
          },
          "about": {
            "type": [
              "string",
              "null"
            ]
          },
          "slogan": {
            "type": [
              "string",
              "null"
            ]
          },
          "enrichment_status": {
            "type": "string",
            "enum": [
              "pending",
              "enriched",
              "failed"
            ]
          },
          "headquarters": {
            "$ref": "#/components/schemas/Headquarters"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "redirect": {
            "type": "object",
            "description": "Present only when the domain you sent resolved to a different\ncanonical domain (e.g. a redirect or a LinkedIn-reported\nshortlink).\n",
            "properties": {
              "from": {
                "type": "string"
              },
              "to": {
                "type": "string"
              },
              "confirmed": {
                "type": "boolean"
              }
            },
            "required": [
              "from",
              "to",
              "confirmed"
            ]
          }
        },
        "required": [
          "name",
          "enrichment_status"
        ]
      },
      "PersonPosition": {
        "type": "object",
        "description": "One role from a person's employment history.\n\n`company` is null more often than you might expect. A LinkedIn\nexperience entry gives a free-text employer name and rarely anything\nwe can resolve to a company record. Read `company_name`. It is\npopulated whenever we have an employer at all. Treat `company` as the\nenrichment on top when we managed to match it.\n",
        "properties": {
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "company_name": {
            "type": [
              "string",
              "null"
            ],
            "description": "The employer's name, resolved if we could match it and as reported otherwise."
          },
          "company": {
            "type": [
              "object",
              "null"
            ],
            "description": "Present only when this role was matched to a company we hold.",
            "properties": {
              "name": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "domain": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "company_linkedin_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "employment_type": {
            "type": [
              "string",
              "null"
            ]
          },
          "location": {
            "type": [
              "string",
              "null"
            ]
          },
          "start_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date",
            "description": "Always a full date, but not always a known one. Read\n`start_date_precision` before displaying it. A source that gave\nonly \"2021\" is stored as `2021-01-01` with precision `year`.\n"
          },
          "start_date_precision": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "year",
              "month",
              "day",
              null
            ],
            "description": "Which components of `start_date` are real rather than padding."
          },
          "end_date": {
            "type": [
              "string",
              "null"
            ],
            "format": "date"
          },
          "end_date_precision": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "year",
              "month",
              "day",
              null
            ]
          },
          "is_current": {
            "type": "boolean"
          }
        },
        "required": [
          "is_current"
        ]
      },
      "EmailMatch": {
        "type": "object",
        "description": "How an `email` request was tied to this person. Present only for email\nlookups. A profile-URL lookup has nothing to qualify.\n",
        "properties": {
          "email": {
            "type": "string"
          },
          "method": {
            "type": "string",
            "enum": [
              "exact",
              "resolved",
              "inferred"
            ],
            "description": "`exact`: the address was already on record against this person.\n`resolved`: the address was resolved live and confirmed against\na profile pulled for this request.\n`inferred`: the local part identified them among the people we\nalready held at that domain.\n"
          },
          "confidence": {
            "type": [
              "number",
              "null"
            ],
            "description": "0-1. How strongly the address identifies this person."
          }
        },
        "required": [
          "email",
          "method"
        ]
      },
      "Person": {
        "type": "object",
        "description": "Present as `data` once a task's `status` is `\"succeeded\"`.",
        "properties": {
          "linkedin_url": {
            "type": "string"
          },
          "linkedin_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "full_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "first_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "last_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "headline": {
            "type": [
              "string",
              "null"
            ]
          },
          "about": {
            "type": [
              "string",
              "null"
            ]
          },
          "avatar_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "country_code": {
            "type": [
              "string",
              "null"
            ]
          },
          "linkedin_followers": {
            "type": [
              "integer",
              "null"
            ]
          },
          "linkedin_connections": {
            "type": [
              "integer",
              "null"
            ]
          },
          "memorialized": {
            "type": "boolean",
            "description": "LinkedIn has marked this profile as belonging to someone who has died."
          },
          "enrichment_status": {
            "type": "string",
            "enum": [
              "pending",
              "enriched",
              "failed"
            ]
          },
          "last_enriched_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "positions": {
            "type": "array",
            "description": "Employment history, current roles first.",
            "items": {
              "$ref": "#/components/schemas/PersonPosition"
            }
          },
          "email_match": {
            "$ref": "#/components/schemas/EmailMatch"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "linkedin_url",
          "enrichment_status",
          "positions"
        ]
      },
      "LoopDestination": {
        "type": "object",
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "webhook"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "enabled": {
            "type": "boolean"
          },
          "secret": {
            "type": "string",
            "description": "Only present in the response to `POST /v1/loops`. Never returned afterwards."
          }
        },
        "required": [
          "kind",
          "url",
          "enabled"
        ]
      },
      "Loop": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "object_type": {
            "type": "string",
            "enum": [
              "company",
              "person"
            ]
          },
          "operation": {
            "type": "string",
            "enum": [
              "enrich"
            ]
          },
          "enabled": {
            "type": "boolean"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "destination": {
            "$ref": "#/components/schemas/LoopDestination"
          }
        },
        "required": [
          "id",
          "name",
          "slug",
          "object_type",
          "operation",
          "enabled",
          "created_at",
          "updated_at"
        ]
      },
      "LoopWithSecret": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Loop"
          }
        ],
        "description": "The one response where `destination.secret` is populated."
      }
    }
  }
}