Operations
An operation is a business event bind.co found in the user's mail: an expense, a payroll change, a purchase financed with a loan. It is the record your application acts on. This guide covers what an operation is built from, its lifecycle from found to decided, what a person can do with one, how duplicates are handled, how documents feed operations, and how the chaser follows up on operations that are missing something.
Operation types
Each kind of operation is a type, such as expenses. GET /v1/operations lists every type with the
scopes it needs, and GET /v1/operations/{operation_type}/schema describes every field; neither needs a
token, so you can read them before your application is approved.
New types appear in that list without a new API version, and their routes follow the same pattern.
Reading a type needs its own scope, such as operations:expenses:read; changing it needs
operations:expenses:write, which includes operations:expenses:read. The examples below use expenses.
What an operation looks like
{
"operation_id": "op_5b1f0c7e-8a2d-4c3b-9e61-2f7d4a9c0b13",
"operation_type": "expenses",
"status": "pending_approval",
"vendor_normalised": {"value": "Delta Air Lines", "bind_inference_confidence": 0.97},
"expense_date": {"value": "2026-10-14", "bind_inference_confidence": 0.99},
"total_amount": {"value": "412.60", "bind_inference_confidence": 0.98},
"currency_code": {"value": "USD", "bind_inference_confidence": 0.99},
"assessment": {"bind_inference_confidence": 0.91, "completeness_state": "complete"},
"chaser_status": null,
"evidence": [
{"evidence_type": "email", "role": "primary", "created_at": "2026-10-14T16:04:30Z", "file": null,
"email": {"email_id": "eml_16nJ9iKYhhC", "email_sender": "receipts@delta.com", "email_timestamp": "2026-10-14T16:01:47Z"}}
],
"schema": {"operation_type": "expenses", "version": 3}
}
- Each business field carries its
valueand abind_inference_confidencebetween 0 and 1; a value a person set has 1. assessmentgives the overall confidence andcompleteness_state:completewhen every required field is filled,incompleteotherwise.evidencelists every email and file the operation was built from; see Emails and Files.schemanames the version of the type's schema the record follows.
Reading confidence
bind_inference_confidence is bind.co's relative confidence in a value, between 0 and 1. Use it to
rank and to choose what a person should look at; it is not a calibrated probability, so 0.9 does not
mean "right nine times in ten". Compare scores within one field, not across fields: 0.9 on an amount
and 0.9 on a drafted email body are not the same kind of certainty. Pick your thresholds on your own
users' data.
A value a person set reads 1. That says who decided, not that the value is certain: a person can
enter a wrong amount. To tell a person's decision from bind.co's reading, use the record's status
(approved, corrected and rejected are a person's decisions) and, on profiles, contacts and
roles, truth_updated_at (set when a person saved it).
Where operations come from
- An email, such as a receipt or a payroll instruction.
- A file, such as an invoice attached to an email.
- Both: a document can start a new operation, or complete one already found in an email.
- Your application:
POST /v1/operations/expensesadds one bind.co did not find. It is stored asapproved, since a person provided it. Sending the same record again returns the existing operation with200instead of a duplicate.
When more mail or files about the same event arrive, they attach to the same operation.
Lifecycle
| Status | Meaning | Returned by reads |
|---|---|---|
active |
Found and open; required fields may still be missing | Yes |
pending_approval |
Every required field is filled; waiting for a person | Yes |
approved |
A person confirmed it as it is | Yes |
corrected |
A person confirmed it with changes | Yes |
rejected |
A person said it is wrong | Yes |
merged |
Folded into another operation | No |
deleted |
Hidden by a person | No |
Until a person decides, bind.co keeps an operation up to date as evidence arrives: an active
operation can fill its missing fields and move to pending_approval. Once a person has confirmed,
corrected or rejected it, new evidence, including evidence from a merge, is still listed under it,
but its values stay as the person left them unless a person resets it.
What a person can do
| Action | Route | From | To |
|---|---|---|---|
| Confirm | POST /v1/operations/expenses/{operation_id}/confirm |
active, pending_approval |
approved |
| Correct | PATCH /v1/operations/expenses/{operation_id} |
active, pending_approval, approved, corrected |
corrected |
| Reject | POST /v1/operations/expenses/{operation_id}/reject |
active, pending_approval |
rejected |
| Delete | DELETE /v1/operations/expenses/{operation_id} |
any status reads return | deleted |
| Restore | POST /v1/operations/expenses/{operation_id}/restore |
rejected, deleted |
the status it had before |
| Reset | POST /v1/operations/expenses/{operation_id}/reset |
approved, corrected |
back to bind.co's values, waiting for a decision |
PATCH is partial: send only the fields you change, and null clears one. Nothing is ever erased:
a deleted operation is kept and can be restored. Reset does not apply to an operation your
application created by hand, since there is nothing bind.co read for it to go back to.
A typical review screen: search with "status": ["pending_approval"] for operations ready to
decide, show each with its evidence, and offer confirm, correct and reject. Search active to show
operations still being completed.
One event, one operation
bind.co checks for duplicates and merges them on its own. A person can merge too:
POST /v1/operations/expenses/{operation_id}/merge with the duplicates in loser_operation_ids. They
must be active, and this operation must not be rejected. The duplicates become merged and their
evidence moves to this operation. What happens to this operation's values depends on its status:
- Not yet decided (
activeorpending_approval): it takes the best version of each field across the merged operations and waits for a decision, asactiveorpending_approval. - Already decided (
approvedorcorrected, which includes one your application created): its values and status stay as they are; it only gains the evidence.
POST /v1/operations/expenses/{operation_id}/unmerge undoes merges, whoever made them: each
operation comes back active with its own id and evidence. Once separated, bind.co never merges that
pair again on its own. If your application stores operation ids, a stored id that starts answering
404 was usually merged into another one.
Documents
Many operations rest on documents. bind.co recognises the documents in each file, such as an
invoice, a receipt or a passport, reads their fields, and uses them to start or complete operations.
In an operation's file evidence, doc_types lists each document with its kind, its fields, and
requirement_set_satisfied, which says whether it has every field its kind requires. An operation
type can also require documents; a missing or incomplete required document is one of the gaps the
chaser follows up on.
The chaser
An operation that is still active and incomplete is missing something: a field, a value bind.co
is not sure enough about, or a required document. The chaser is bind.co's follow-up on those gaps.
Not every operation type is chased, and an operation the chaser has never looked at has a null
chaser_status.
It works in three phases, in order.
1. self_help. bind.co looks for the missing value in the user's other operations and the files
already in their mailbox, and offers candidates:
{
"operation_id": "op_5b1f0c7e-8a2d-4c3b-9e61-2f7d4a9c0b13",
"chaser_status": "in_self_help",
"gaps": [{
"canonical_path": "expense_date",
"reason": "absent",
"candidates": [{"canonical_path": "expense_date", "source_operation_id": "op_3f6c2a9e-8b1d-4c57-9e0a-5d2f7b8c1e44",
"observed": {"value": "2026-10-14", "bind_inference_confidence": 0.94}}],
"document_candidates": []
}]
}
Accepting a candidate (.../chaser/self-help/confirm) adds its evidence to the operation and cannot
be undone. Declining it (.../chaser/self-help/reject) removes it from the list for good.
2. counterpart_outreach. bind.co prepares a message asking the other party for what is missing:
{
"operation_id": "op_5b1f0c7e-8a2d-4c3b-9e61-2f7d4a9c0b13",
"chaser_status": "pending_approval",
"counterpart_name": "Dana Ruiz",
"counterpart_email": "dana@acme.com",
"missing_item": "the invoice date",
"subject": "Invoice INV-2207: date missing",
"body": "Hi Dana, could you confirm the date on invoice INV-2207? Thanks, Maya"
}
A person approves it (.../chaser/outreach-approval/confirm) or rejects it
(.../chaser/outreach-approval/reject), optionally with one of the operation's allowed reasons.
Approving records the decision; it sends nothing. One decision per operation. Seeing these messages
needs drafts:read, and deciding needs drafts:write (which includes drafts:read), as well as the
operation type's scope.
3. owner_escalation. bind.co hands the operation to the user, with what is still missing. They
fill it with the correct and confirm routes above, or stop the chase with
.../chaser/owner-escalation/dismiss, which leaves the operation itself unchanged.
Each phase has a queue, for example GET /v1/operations/expenses/chaser/self-help, and
GET /v1/operations/expenses/{operation_id}/chaser shows where one operation stands: its phase, its
gaps and its next step. Search with chaser_status to find operations in a given state.
Webhooks
<operation_type>.first_completed: the user's first operations of a type are ready. Sent once per user.<operation_type>.completed: more operations of a type were found or updated.
{"type": "expenses.completed", "data": {"user_inbox_fetch_id": "fch_4hT0q2Lw9Za", "operation_type": "expenses", "operations_created": 3, "operations_updated": 5}}
operations_created counts new operations; operations_updated counts emails and files that
attached to existing ones, so one operation can count more than once. Your application receives these events only when it
holds the scopes the type needs. On each one, search the type again, for example sorted by newest.
The webhooks guide has every payload.