# Pull from a vendor (/build/connector-pull)

Write a vendor's records into Marfa as items, write only what changed, and say which types the connector holds read only.



A connector pulls: it reads the vendor and writes what it finds as items. A two-way connector also carries changes made in Marfa back, as [Carry changes back](/build/connector-push) shows, and a one-way connector says which types it holds read only.

The operations below come in the order a run uses them, for a connector built directly on the API. [The connector kit](/build/connector) does all of this for you, [read-only types](#declare-the-types-you-dont-carry-back) included.

The examples follow a task manager connector with a key of its own under the source `tasks`, which writes the type `tasks.task`. In the TypeScript examples, `marfa` is a [TypeScript SDK](/typescript-sdk) client made with that key.

## Write what you read [#write-what-you-read]

A pull reads the vendor from where the last run stopped, which the connector keeps in its state document, and writes what it read. [Upsert items in bulk](/api-reference/items/bulkUpsertItems) writes up to 5,000 records in one request:



**HTTP**

```http  
POST /items/bulk
Authorization: Bearer <key>
Content-Type: application/json

{
  "items": [
    {
      "type": "tasks.task",
      "source_id": "acct-7:4012",
      "tier": "feed",
      "properties": { "task_id": "4012", "title": "Renew passport", "status": "not_started" },
      "occurred_at": "2026-10-01T08:00:00Z"
    }
  ],
  "atomic": false
}
```

**TypeScript**

```ts
const { data, error } = await marfa.POST("/items/bulk", {
  body: {
    items: [
      {
        type: "tasks.task",
        source_id: "acct-7:4012",
        tier: "feed",
        properties: { task_id: "4012", title: "Renew passport", status: "not_started" },
        occurred_at: "2026-10-01T08:00:00Z",
      },
    ],
    atomic: false,
  },
});
```

Marfa answers each record on its own:

Response (200 OK):

```json
{
  "counts": { "created": 1, "updated": 0, "skipped": 0, "errored": 0 },
  "results": [{ "index": 0, "outcome": "created", "id": "01a12313-fd4a-78a1-af1e-32792c3df63b" }]
}
```

An item's natural key is its `source`, from the connector's key, and its `source_id`, the vendor's own ID for the record. Marfa updates the item that holds the key, or creates one, so reading a record twice never makes a second item. Put the **vendor account** inside the `source_id`, as in `acct-7:4012`, so two accounts under one source can't share an item.

`tier: "feed"` puts the item in the feed, where a person or an app decides whether to keep it. `occurred_at` is when the record happened at the vendor. `atomic: false` writes the records that pass and reports a refused one as `errored`; by default, one refused record **rolls the whole batch back**. Marfa answers reads while a page is written. Under the default, a read shows each item as it was before the page until the page ends, and **other writes wait for the page**, so a large atomic page holds up every other writer.

**Write an item's text as Markdown.** Marfa stores the field its type names in [`display_hints.body_field`](/catalog/display-hints#the-hints) unchecked, so convert a vendor's HTML first, or an app shows the tags.

## Write only what changed [#write-only-what-changed]

A write of the properties an item already holds **still raises its `version`** and announces `item.updated`, so a pull that rewrites every record makes noise for everyone who follows the event stream.

Before you write, [look up the items](/api-reference/items/lookupItems) you hold under the records' keys and **send only the records that differ**:



**HTTP**

```http  
POST /items/lookup
Authorization: Bearer <key>
Content-Type: application/json

{ "type": "tasks.task", "source": "tasks", "source_ids": ["acct-7:4012", "acct-7:4013"] }
```

**TypeScript**

```ts
const { data } = await marfa.POST("/items/lookup", {
  body: { type: "tasks.task", source: "tasks", source_ids: ["acct-7:4012", "acct-7:4013"] },
});
```

Two cases need care:

* **An item in the bin:** a record that matches it comes back `skipped`, with the reason `trashed`.
* **A purged item:** it leaves a tombstone, which the lookup returns beside the items. Write a purged record again only if the vendor changed it after the tombstone's `settled_at`.

## Keep a quiet run light [#keep-a-quiet-run-light]

A run that finds nothing new at the vendor should send Marfa **a few requests, however much the connector holds**. Ask about many records at once: one lookup names many `source_ids`, and [a listing](/api-reference/items/listItems) reads up to 500 items a page. Never send a request for each item you hold.

In the kit, `linkedEach` asks about many targets in one call, so use it rather than `linked` in a loop, and call `held` once for each type in a run. Every connector in marfa-connectors keeps the kit's shared quiet-run test, which fails when a run with nothing new sends Marfa more than one more request for each 200 rows Marfa holds.

## Set aside what the vendor deleted [#set-aside-what-the-vendor-deleted]

When the vendor no longer has a record, [change the item's state](/api-reference/items/transitionItem) to `archived`. **Don't move it to the bin**: the bin is for people, and an archived item keeps its history and can be made active again if the record comes back.



**HTTP**

```http  
POST /items/01a12313-fd4e-7800-ab67-c886f1b9ef58/transition
Authorization: Bearer <key>
Content-Type: application/json

{ "state": "archived" }
```

**TypeScript**

```ts
await marfa.POST("/items/{id}/transition", {
  params: { path: { id: "01a12313-fd4e-7800-ab67-c886f1b9ef58" } },
  body: { state: "archived" },
});
```

Be **sure the vendor deleted it**: a partial listing, such as open tasks only, says nothing about a record it leaves out, so ask about that record first.

## Declare the types you don't carry back [#declare-the-types-you-dont-carry-back]

A connector that takes no edits back for a type puts back, on its next run, any edit a person makes to the type's items in Marfa. Name those types in `read_only_types` when you [register the connector](/api-reference/connectors/registerConnector), and their items read `read_only: true`, so an app can open them locked:



**HTTP**

```http  
POST /connectors
Authorization: Bearer <key>
Content-Type: application/json

{ "name": "tasks", "read_only_types": ["tasks.project"] }
```

**TypeScript**

```ts
await marfa.POST("/connectors", {
  body: { name: "tasks", read_only_types: ["tasks.project"] },
});
```

* **Each registration replaces the list**, and one without it clears the list. Build it on every start from what the connector carries back with the settings it runs on. **The kit does this for you**: it lists every type the connector writes and doesn't carry back, so a settings change reaches the list when the connector restarts.
* **An item reads `true` only while every connector** whose live key owns or claims its source lists its type and may write it. Once that stops, its next read says `false`.
* **Marfa doesn't refuse an edit** to a read-only item. The flag says what the connector will do.
* **The list is per type.** Leave a type out when you carry back any of its fields or items.

The rules behind this page are in the [connectors](https://github.com/withmarfa/marfa/blob/main/conformance/spec/connectors.md) and [items](https://github.com/withmarfa/marfa/blob/main/conformance/spec/items.md#read-only) chapters of the contract.
