openapi: 3.1.0 info: title: Origami API version: "1.0" description: | The Origami API lets you enrich company and people data programmatically. See the [Quickstart](/quickstart) for a walkthrough with curl examples. All write endpoints are **asynchronous** — a `POST` returns a `batchId` immediately. Poll `GET /batches/{batchId}` for progress and enriched results. ## Rate limits All endpoints are rate-limited **per organization**. | Scope | Limit | |-------|-------| | All endpoints | **100 requests / minute** | | `POST /tables/{tableId}/rows` | **10 requests / minute** | Every response includes `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset` headers. ## Request IDs Every response includes an `X-Request-Id` header. You can pass your own ID via the `X-Request-Id` request header (1–64 alphanumeric / dash / underscore characters); otherwise the server generates a UUID. Include this ID in support requests to help us trace issues. ## Errors All errors follow a consistent format: ```json { "error": "Human-readable error message", "code": "MACHINE_READABLE_CODE" } ``` Some errors include additional context fields like `path`, `fields`, `hint`, `current`, `limit`, or `requested`. | Code | Status | Meaning | |------|--------|---------| | `UNAUTHORIZED` | 401 | Missing, invalid, revoked, or expired API key. | | `VALIDATION_ERROR` | 400 | Invalid request parameter or body. | | `NOT_FOUND` | 404 | Table or batch not found. | | `RATE_LIMITED` | 429 | Too many requests — see rate-limit headers. | | `INSUFFICIENT_CREDITS` | 402 | Not enough credits to complete the operation. | | `SUBSCRIPTION_REQUIRED` | 402 | Feature requires a plan upgrade. | | `INTERNAL_ERROR` | 500 | Unexpected server error. | contact: name: Origami Support url: https://origami.chat license: name: Proprietary servers: - url: https://origami.chat/api/v1 description: Production security: - apiKey: [] tags: - name: Tables description: List tables and read row data. - name: Rows description: Insert rows and trigger enrichment. - name: Batches description: Poll async batch progress and retrieve results. - name: Credits description: Check your organization's credit balance. paths: /tables: get: operationId: listTables summary: List tables description: | Returns all tables in the organization with column metadata. The API key scopes to the org — no workspace ID needed. The typical workflow is: list tables → find the one you want by name → note its input columns → use the `tableId` and column slugs in `POST /tables/{tableId}/rows`. tags: [Tables] parameters: - $ref: "#/components/parameters/page" - $ref: "#/components/parameters/pageSize" responses: "200": description: Tables retrieved successfully. content: application/json: schema: type: object required: [tables, total, page, pageSize] properties: tables: type: array items: $ref: "#/components/schemas/Table" total: type: integer description: Total number of tables in the organization. page: type: integer description: Current page index (zero-based). pageSize: type: integer description: Number of tables per page. example: tables: - id: "d290f1ee-6c54-4b01-90e6-d701748f0851" name: "Series A SaaS Companies" workspaceId: "7c9e6679-7425-40de-944b-e07fc1f90ae7" workspaceName: "Outbound Q2" rowCount: 150 columns: - name: "Company Name" slug: "company-name" kind: "input" - name: "Website" slug: "website" kind: "input" - name: "CEO Email" slug: "ceo-email" kind: "enrichment" autoTrigger: true - name: "Quality Score" slug: "quality-score" kind: "score" defaultFilters: - column: "website" operator: "is_not_empty" value: "" defaultSort: column: "quality-score" direction: "desc" createdAt: "2025-06-01T12:00:00Z" updatedAt: "2025-06-15T09:30:00Z" total: 1 page: 0 pageSize: 50 "401": $ref: "#/components/responses/Unauthorized" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/InternalError" /tables/{tableId}/rows: get: operationId: getTableRows summary: Read table rows description: | Read all rows in a table with pagination, filtering, and sorting. Rows are returned as flat objects with column **slugs** as keys. The response includes a `columns` map (slug → display name) so you can resolve human-readable names. Cells with no value or in an errored state are omitted. Supports CSV export via the `format` query parameter. tags: [Tables] parameters: - $ref: "#/components/parameters/tableId" - $ref: "#/components/parameters/page" - name: pageSize in: query schema: type: integer default: 50 minimum: 1 maximum: 500 description: Number of rows per page (max 500). - name: filters in: query schema: type: string description: | JSON-encoded array of filter objects. Each filter uses a column **slug** (from `GET /tables`), an `operator`, and a `value`. Operators: `contains`, `not_contains`, `equals`, `not_equals`, `is_empty`, `is_not_empty`, `greater_than`, `greater_than_or_equal`, `less_than`, `less_than_or_equal`. When provided, the table's default filters are **replaced** by these. example: '[{"column":"website","operator":"is_not_empty","value":""}]' - name: sort in: query schema: type: string description: | JSON-encoded sort object with a column **slug** and direction. When provided, the table's default sort is **replaced** by this. example: '{"column":"quality-score","direction":"desc"}' - name: columns in: query schema: type: string description: | Comma-separated list of column slugs to include in the response. Omit to return all columns. Use slugs from `GET /tables`. example: "company-name,website,quality-score" - name: defaults in: query schema: type: string enum: ["true", "false"] default: "true" description: | Whether to apply the table's saved filters and sort order (the same ones shown in the Origami dashboard). Set to `false` to get all rows unfiltered, in insertion order. - name: format in: query schema: type: string enum: [json, csv] default: json description: Response format. Use `csv` for spreadsheet-compatible export. responses: "200": description: Rows retrieved successfully. content: application/json: schema: $ref: "#/components/schemas/RowsResponse" example: columns: company-name: "Company Name" website: "Website" ceo-email: "CEO Email" rows: - id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" company-name: "Acme Corp" website: "acme.com" ceo-email: "ceo@acme.com" total: 1234 page: 0 pageSize: 50 text/csv: schema: type: string "400": description: Invalid query parameters. content: application/json: schema: $ref: "#/components/schemas/Error" examples: invalidFilters: summary: Malformed filters JSON value: error: "Invalid filters JSON" code: "INVALID_FILTERS" invalidSort: summary: Malformed sort JSON value: error: "Invalid sort JSON" code: "INVALID_SORT" unknownColumn: summary: Filter or sort references an unknown column slug value: error: "Unknown filter columns" code: "UNKNOWN_COLUMN" columns: ["nonexistent-column"] validationError: summary: Invalid filter operator or sort direction value: error: "Invalid filter operator" code: "VALIDATION_ERROR" operators: ["bad_operator"] "401": $ref: "#/components/responses/Unauthorized" "404": $ref: "#/components/responses/TableNotFound" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/InternalError" post: operationId: insertRows summary: Insert rows description: | Insert rows into an existing table. Returns a batch ID immediately — poll `GET /batches/{batchId}` for enrichment progress and results. **Column matching:** Field names in each row object are matched to **input** columns by **slug** (exact match). Only columns with `kind: "input"` are accepted — enrichment and score columns are populated automatically. Use `GET /tables` to discover available column slugs and their kinds. **Enrichment:** By default (`enrich: true`), all auto-trigger enrichment columns run immediately after insertion. Set `enrich: false` to insert data without triggering enrichment. **Deduplication & exclusion lists** apply automatically — the same table-level rules from the UI are enforced. Duplicate or excluded rows are inserted but may not enrich. **Limits:** - Maximum **100 rows** per request. - Free-plan tables are capped at **30 rows** total. tags: [Rows] parameters: - $ref: "#/components/parameters/tableId" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/InsertRowsRequest" examples: basic: summary: Insert two companies value: rows: - company-name: "Acme Corp" website: "acme.com" - company-name: "Beta Inc" website: "beta.io" withoutEnrichment: summary: Insert without triggering enrichment value: rows: - company-name: "Gamma LLC" website: "gamma.dev" enrich: false idempotentRetry: summary: Idempotent insert with caller-generated batch ID value: rows: - company-name: "Acme Corp" website: "acme.com" batchId: "550e8400-e29b-41d4-a716-446655440000" responses: "200": description: | Idempotent replay — a batch with the provided `batchId` already exists. The original batch ID is returned without inserting new rows. content: application/json: schema: $ref: "#/components/schemas/BatchCreated" example: batchId: "550e8400-e29b-41d4-a716-446655440000" "201": description: Rows accepted for processing. content: application/json: schema: $ref: "#/components/schemas/BatchCreated" example: batchId: "f47ac10b-58cc-4372-a567-0e02b2c3d479" "400": description: Validation error. content: application/json: schema: $ref: "#/components/schemas/Error" examples: nonInputColumns: summary: Request contains enrichment or score column slugs value: error: "Only input columns can be set via the API" code: "NON_INPUT_COLUMNS" fields: ["ceo-email"] hint: "Enrichment and score columns are populated automatically" unknownFields: summary: Request contains fields that don't match any column slug value: error: "Unknown fields" code: "UNKNOWN_FIELDS" fields: ["Foo", "Bar"] hint: "Use input column slugs from GET /tables" rowLimit: summary: Table has reached its plan's row limit value: error: "Row limit exceeded" code: "ROW_LIMIT_EXCEEDED" current: 28 limit: 30 requested: 5 validationError: summary: Invalid request body (e.g. empty rows, too many rows, invalid batchId) value: error: "rows must be a non-empty array" code: "VALIDATION_ERROR" path: "rows" "401": $ref: "#/components/responses/Unauthorized" "402": $ref: "#/components/responses/PaymentRequired" "404": $ref: "#/components/responses/TableNotFound" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/InternalError" /batches/{batchId}: get: operationId: getBatch summary: Get batch description: | Check the status of an async batch. When all enrichments are complete, the response includes the full enriched row data and total credits used. A batch is `"complete"` when every enrichment has reached a terminal state — this includes both succeeded and failed runs. Check `enrichments.failed > 0` to detect partial failures. **Polling strategy:** For fast results, poll every 2–5 seconds. For background processing, poll every 30–60 seconds. tags: [Batches] parameters: - name: batchId in: path required: true schema: type: string format: uuid description: The batch ID returned by a write operation. responses: "200": description: Batch status retrieved. content: application/json: schema: $ref: "#/components/schemas/Batch" examples: processing: summary: Batch still processing value: batchId: "f47ac10b-58cc-4372-a567-0e02b2c3d479" tableId: "d290f1ee-6c54-4b01-90e6-d701748f0851" type: "insert" status: "processing" rowCount: 2 enrichments: total: 10 completed: 4 pending: 6 failed: 0 failures: {} creditsUsed: 0 createdAt: "2026-07-01T14:30:00Z" completedAt: null complete: summary: Batch complete with enriched data value: batchId: "f47ac10b-58cc-4372-a567-0e02b2c3d479" tableId: "d290f1ee-6c54-4b01-90e6-d701748f0851" type: "insert" status: "complete" rowCount: 2 enrichments: total: 10 completed: 10 pending: 0 failed: 0 failures: {} columns: company-name: "Company Name" website: "Website" ceo-email: "CEO Email" employee-count: "Employee Count" rows: - id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" company-name: "Acme Corp" website: "acme.com" ceo-email: "ceo@acme.com" employee-count: 150 - id: "b2c3d4e5-f6a7-8901-bcde-f12345678901" company-name: "Beta Inc" website: "beta.io" ceo-email: "founder@beta.io" employee-count: 42 creditsUsed: 12 createdAt: "2026-07-01T14:30:00Z" completedAt: "2026-07-01T14:31:15Z" partialFailure: summary: Batch complete with some failures value: batchId: "f47ac10b-58cc-4372-a567-0e02b2c3d479" tableId: "d290f1ee-6c54-4b01-90e6-d701748f0851" type: "insert" status: "complete" rowCount: 5 enrichments: total: 10 completed: 6 pending: 0 failed: 4 failures: insufficientCredits: 3 enrichmentError: 1 columns: company-name: "Company Name" website: "Website" ceo-email: "CEO Email" rows: - id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" company-name: "Acme Corp" website: "acme.com" ceo-email: "ceo@acme.com" creditsUsed: 36 createdAt: "2026-07-01T14:30:00Z" completedAt: "2026-07-01T14:32:00Z" "400": description: Invalid path parameter. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: "Invalid batchId" code: "VALIDATION_ERROR" "401": $ref: "#/components/responses/Unauthorized" "404": description: Batch not found. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: "Batch not found" code: "NOT_FOUND" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/InternalError" /credits: get: operationId: getCredits summary: Get credit balance description: | Returns the current credit balance for the organization tied to this API key. tags: [Credits] responses: "200": description: Credit balance retrieved. content: application/json: schema: $ref: "#/components/schemas/Credits" example: balance: 1250 currency: "credits" "401": $ref: "#/components/responses/Unauthorized" "429": $ref: "#/components/responses/RateLimited" "500": $ref: "#/components/responses/InternalError" components: securitySchemes: apiKey: type: http scheme: bearer bearerFormat: "og_live_*" description: | API key with `og_live_` prefix. Create keys in **Settings → API Keys**. Pass in the `Authorization` header: ``` Authorization: Bearer og_live_abc123... ``` parameters: tableId: name: tableId in: path required: true schema: type: string format: uuid description: The table UUID. Use `GET /tables` to list available tables. page: name: page in: query schema: type: integer default: 0 minimum: 0 description: Zero-based page index. pageSize: name: pageSize in: query schema: type: integer default: 50 minimum: 1 maximum: 100 description: Number of items per page. schemas: Table: type: object required: [id, name, workspaceId, workspaceName, rowCount, columns, createdAt, updatedAt] properties: id: type: string format: uuid name: type: string description: Table display name. workspaceId: type: string format: uuid workspaceName: type: string description: Name of the workspace containing the table. rowCount: type: integer description: Number of active (non-deleted) rows. columns: type: array items: $ref: "#/components/schemas/Column" defaultFilters: type: array nullable: true description: | Filters currently applied to this table in the Origami dashboard. `null` when no filters are active. Column references use slugs. items: type: object required: [column, operator, value] properties: column: type: string description: Column slug. operator: type: string description: Filter operator (e.g. `equals`, `contains`, `is_not_empty`). value: description: Filter value. defaultSort: type: object nullable: true description: | Sort order currently applied to this table in the Origami dashboard. `null` when no sort is active. required: [column, direction] properties: column: type: string description: Column slug. direction: type: string enum: [asc, desc] createdAt: type: string format: date-time updatedAt: type: string format: date-time Column: type: object required: [name, slug, kind] properties: name: type: string description: Human-readable column name. slug: type: string description: | URL-safe identifier. Unique per table. Used as the key in row data objects and as the field name in `POST /tables/{tableId}/rows`. Slugs are stable across column renames. kind: type: string enum: [input, enrichment, score] description: | Column type: - `input` — user-provided data (e.g. company name, website) - `enrichment` — auto-populated by Origami enrichment services - `score` — computed quality/relevance score autoTrigger: type: boolean description: | Only present on `enrichment` columns. When `true`, this enrichment runs automatically when new rows are inserted. When `false`, it must be triggered manually from the Origami UI. Row: type: object required: [id] properties: id: type: string format: uuid description: Row UUID. additionalProperties: true description: | Flat object with column **slugs** as keys. Values are strings, numbers, or arrays depending on column type. Cells with no value are omitted. Use the `columns` map in the response to resolve slugs to display names. InsertRowsRequest: type: object required: [rows] properties: rows: type: array minItems: 1 maxItems: 100 items: type: object additionalProperties: true description: | Key-value pairs where keys are **input** column slugs (from `GET /tables`, where `kind` is `"input"`) and values are the cell data to insert. description: | Array of row objects to insert. Each row is a flat object mapping input column slugs to values. Maximum 100 rows per request. enrich: type: boolean default: true description: | Whether to trigger enrichment after inserting rows. Defaults to `true`. Set to `false` to insert data without running enrichment — the batch will immediately be `"complete"`. batchId: type: string format: uuid description: | Optional caller-generated UUID for idempotent retries. If a batch with this ID already exists for the organization, the server returns the existing batch (HTTP 200) without inserting rows again. Generate a UUID client-side and include it with every attempt to make retries safe after network failures. BatchCreated: type: object required: [batchId] properties: batchId: type: string format: uuid description: | Unique identifier for the batch. Use this to poll `GET /batches/{batchId}` for progress and results. Batch: type: object required: [batchId, tableId, type, status, rowCount, enrichments, failures, creditsUsed, createdAt, completedAt] properties: batchId: type: string format: uuid tableId: type: string format: uuid type: type: string enum: [insert] description: The operation type that created this batch. status: type: string enum: [processing, complete, errored] description: | - `processing` — enrichment is still running - `complete` — all enrichments finished (check `enrichments.failed` for partial failures) - `errored` — the batch failed to start (e.g. enrichment pipeline error) rowCount: type: integer description: | Number of rows created by this batch. Each row can have multiple enrichment cells (one per enrichment column), so `enrichments.total` will typically be higher than `rowCount`. enrichments: $ref: "#/components/schemas/BatchEnrichments" columns: type: object additionalProperties: type: string description: | Map of column slug → display name. Only present when `status` is `"complete"`. Use this to resolve slug keys in `rows` to human-readable column names. rows: type: array items: $ref: "#/components/schemas/Row" description: | Enriched row data. Only present when `status` is `"complete"`. Row keys are column slugs — use the `columns` map to resolve display names. failures: $ref: "#/components/schemas/BatchFailures" creditsUsed: type: integer description: Total credits consumed by this batch (rounded to nearest whole credit). `0` while processing or when no credits were used. createdAt: type: string format: date-time completedAt: type: string format: date-time nullable: true description: Timestamp when the batch finished. `null` while still processing. BatchEnrichments: type: object required: [total, completed, pending, failed] properties: total: type: integer description: Total enrichment cells in the batch (one per enrichment column per row). completed: type: integer description: Enrichment cells that finished successfully. pending: type: integer description: Enrichment cells still waiting or in progress. failed: type: integer description: Enrichment cells that errored. Check `failures` for breakdown. BatchFailures: type: object description: | Breakdown of failed enrichment cells by cause. Empty object `{}` when there are no failures. properties: insufficientCredits: type: integer description: Cells that failed because the org ran out of credits. Top up and re-run. enrichmentError: type: integer description: Cells that failed due to an enrichment service error (bad input, upstream API down, etc.). Credits: type: object required: [balance, currency] properties: balance: type: integer description: Current credit balance for the organization (rounded to nearest whole credit). currency: type: string enum: [credits] RowsResponse: type: object required: [columns, rows, total, page, pageSize] properties: columns: type: object additionalProperties: type: string description: | Map of column slug → display name. Use this to resolve the slug keys in each row object to human-readable column names. rows: type: array items: $ref: "#/components/schemas/Row" total: type: integer description: Total number of rows matching the query (before pagination). page: type: integer description: Current page index (zero-based). pageSize: type: integer description: Number of rows per page. Error: type: object required: [error, code] properties: error: type: string description: Human-readable error message. code: type: string description: Machine-readable error code. additionalProperties: true responses: Unauthorized: description: Missing, invalid, revoked, or expired API key. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: "Invalid API key" code: "UNAUTHORIZED" TableNotFound: description: Table not found or does not belong to this organization. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: "Table not found" code: "NOT_FOUND" PaymentRequired: description: Insufficient credits or subscription upgrade needed. content: application/json: schema: $ref: "#/components/schemas/Error" examples: insufficientCredits: summary: Not enough credits value: error: "Insufficient credits" code: "INSUFFICIENT_CREDITS" creditsRequired: 50 creditsAvailable: 12 subscriptionRequired: summary: Plan upgrade needed value: error: "Subscription required" code: "SUBSCRIPTION_REQUIRED" hint: "Upgrade to a Growth or Business plan to use this feature." InternalError: description: Unexpected server error. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: "Internal server error" code: "INTERNAL_ERROR" RateLimited: description: Rate limit exceeded. headers: X-RateLimit-Limit: schema: type: integer description: Maximum requests allowed in the window. X-RateLimit-Remaining: schema: type: integer description: Requests remaining in the current window. X-RateLimit-Reset: schema: type: integer description: Unix timestamp when the rate limit window resets. content: application/json: schema: $ref: "#/components/schemas/Error" example: error: "Rate limit exceeded" code: "RATE_LIMITED"