# Productive API > Productive is a Professional Services Automation platform. This file is the complete resource map following https://llmstxt.org. Each entry below is a filtered OpenAPI spec URL containing full schemas, parameters, and examples for that resource group. Base URL: https://api.productive.io/api/v2/ ## Auth (required on every request) X-Auth-Token: {token} (Settings → API integrations → Generate new token) X-Organization-Id: {org_id} (numeric, from your Productive URL) Content-Type: application/vnd.api+json (required on POST/PATCH/DELETE) 401 = unauthenticated | 403 = unauthorized ## OpenAPI Spec Full YAML: https://developer.productive.io/reference/download_spec Full JSON: https://developer.productive.io/reference/download_spec?format=json Filter params — append any combination to the spec URL: ?category={slug} all endpoints in a category ?group={slug} all endpoints for one resource ?path=/api/v2/tasks all methods on a path ?path=/api/v2/tasks&method=post single operation ## Resource Groups ### Organization custom-domains: https://developer.productive.io/reference/download_spec?group=custom-domains invitations: https://developer.productive.io/reference/download_spec?group=invitations organization-memberships: https://developer.productive.io/reference/download_spec?group=organization-memberships organizations: https://developer.productive.io/reference/download_spec?group=organizations permission-sets: https://developer.productive.io/reference/download_spec?group=permission-sets sessions: https://developer.productive.io/reference/download_spec?group=sessions subsidiaries: https://developer.productive.io/reference/download_spec?group=subsidiaries users: https://developer.productive.io/reference/download_spec?group=users ### Core Resources activities: https://developer.productive.io/reference/download_spec?group=activities approval-policy: https://developer.productive.io/reference/download_spec?group=approval-policy approval-policy-assignment: https://developer.productive.io/reference/download_spec?group=approval-policy-assignment approval-status: https://developer.productive.io/reference/download_spec?group=approval-status approval-workflow: https://developer.productive.io/reference/download_spec?group=approval-workflow attachments: https://developer.productive.io/reference/download_spec?group=attachments comments: https://developer.productive.io/reference/download_spec?group=comments custom-field-options: https://developer.productive.io/reference/download_spec?group=custom-field-options custom-fields: https://developer.productive.io/reference/download_spec?group=custom-fields custom-field-section: https://developer.productive.io/reference/download_spec?group=custom-field-section emails: https://developer.productive.io/reference/download_spec?group=emails memberships: https://developer.productive.io/reference/download_spec?group=memberships notifications: https://developer.productive.io/reference/download_spec?group=notifications team-memberships: https://developer.productive.io/reference/download_spec?group=team-memberships teams: https://developer.productive.io/reference/download_spec?group=teams templates: https://developer.productive.io/reference/download_spec?group=templates ### Projects placeholders: https://developer.productive.io/reference/download_spec?group=placeholders placeholder-usages: https://developer.productive.io/reference/download_spec?group=placeholder-usages project-milestones: https://developer.productive.io/reference/download_spec?group=project-milestones project-preferences: https://developer.productive.io/reference/download_spec?group=project-preferences projects: https://developer.productive.io/reference/download_spec?group=projects ### Tasks folders: https://developer.productive.io/reference/download_spec?group=folders taskdependency: https://developer.productive.io/reference/download_spec?group=taskdependency task-lists: https://developer.productive.io/reference/download_spec?group=task-lists tasks: https://developer.productive.io/reference/download_spec?group=tasks todos: https://developer.productive.io/reference/download_spec?group=todos workflows: https://developer.productive.io/reference/download_spec?group=workflows workflow-statuses: https://developer.productive.io/reference/download_spec?group=workflow-statuses ### Docs discussions: https://developer.productive.io/reference/download_spec?group=discussions pages: https://developer.productive.io/reference/download_spec?group=pages pageversions: https://developer.productive.io/reference/download_spec?group=pageversions ### Forms survey-field-options: https://developer.productive.io/reference/download_spec?group=survey-field-options survey-fields: https://developer.productive.io/reference/download_spec?group=survey-fields survey-responses: https://developer.productive.io/reference/download_spec?group=survey-responses surveys: https://developer.productive.io/reference/download_spec?group=surveys ### Time Tracking time-entries: https://developer.productive.io/reference/download_spec?group=time-entries time-entries-bulk: https://developer.productive.io/reference/download_spec?group=time-entries-bulk time-entry-versions: https://developer.productive.io/reference/download_spec?group=time-entry-versions timers: https://developer.productive.io/reference/download_spec?group=timers timesheets: https://developer.productive.io/reference/download_spec?group=timesheets time-tracking-policies: https://developer.productive.io/reference/download_spec?group=time-tracking-policies ### Financials bankaccounts: https://developer.productive.io/reference/download_spec?group=bankaccounts bills: https://developer.productive.io/reference/download_spec?group=bills contracts: https://developer.productive.io/reference/download_spec?group=contracts deal-cost-rates: https://developer.productive.io/reference/download_spec?group=deal-cost-rates deals: https://developer.productive.io/reference/download_spec?group=deals document-styles: https://developer.productive.io/reference/download_spec?group=document-styles document-types: https://developer.productive.io/reference/download_spec?group=document-types exchange-rates: https://developer.productive.io/reference/download_spec?group=exchange-rates expense-line-items: https://developer.productive.io/reference/download_spec?group=expense-line-items expense-line-items-bulk: https://developer.productive.io/reference/download_spec?group=expense-line-items-bulk expenses: https://developer.productive.io/reference/download_spec?group=expenses expenses-bulk: https://developer.productive.io/reference/download_spec?group=expenses-bulk overheads: https://developer.productive.io/reference/download_spec?group=overheads prices: https://developer.productive.io/reference/download_spec?group=prices proposals: https://developer.productive.io/reference/download_spec?group=proposals purchase-orders: https://developer.productive.io/reference/download_spec?group=purchase-orders purchase-orders-bulk: https://developer.productive.io/reference/download_spec?group=purchase-orders-bulk rate-cards: https://developer.productive.io/reference/download_spec?group=rate-cards revenue-distributions: https://developer.productive.io/reference/download_spec?group=revenue-distributions salaries: https://developer.productive.io/reference/download_spec?group=salaries sections: https://developer.productive.io/reference/download_spec?group=sections service-assignments: https://developer.productive.io/reference/download_spec?group=service-assignments services: https://developer.productive.io/reference/download_spec?group=services service-type-assignments: https://developer.productive.io/reference/download_spec?group=service-type-assignments service-types: https://developer.productive.io/reference/download_spec?group=service-types taxrates: https://developer.productive.io/reference/download_spec?group=taxrates ### Invoicing automatic-invoicing-rules: https://developer.productive.io/reference/download_spec?group=automatic-invoicing-rules e-invoice-identities: https://developer.productive.io/reference/download_spec?group=e-invoice-identities e-invoice-transactions: https://developer.productive.io/reference/download_spec?group=e-invoice-transactions invoice-attributions: https://developer.productive.io/reference/download_spec?group=invoice-attributions invoices: https://developer.productive.io/reference/download_spec?group=invoices invoice-templates: https://developer.productive.io/reference/download_spec?group=invoice-templates kpd-codes: https://developer.productive.io/reference/download_spec?group=kpd-codes line-items: https://developer.productive.io/reference/download_spec?group=line-items line-items-bulk: https://developer.productive.io/reference/download_spec?group=line-items-bulk payment-reminder-sequences: https://developer.productive.io/reference/download_spec?group=payment-reminder-sequences payments: https://developer.productive.io/reference/download_spec?group=payments ### Resource Management bookings: https://developer.productive.io/reference/download_spec?group=bookings entitlements: https://developer.productive.io/reference/download_spec?group=entitlements events: https://developer.productive.io/reference/download_spec?group=events holiday-calendars: https://developer.productive.io/reference/download_spec?group=holiday-calendars holidays: https://developer.productive.io/reference/download_spec?group=holidays resource-requests: https://developer.productive.io/reference/download_spec?group=resource-requests ### CRM companies: https://developer.productive.io/reference/download_spec?group=companies contact-entries: https://developer.productive.io/reference/download_spec?group=contact-entries deal-contacts: https://developer.productive.io/reference/download_spec?group=deal-contacts deal-contacts-bulk: https://developer.productive.io/reference/download_spec?group=deal-contacts-bulk deal-statuses: https://developer.productive.io/reference/download_spec?group=deal-statuses lost-reasons: https://developer.productive.io/reference/download_spec?group=lost-reasons people: https://developer.productive.io/reference/download_spec?group=people pipelines: https://developer.productive.io/reference/download_spec?group=pipelines ### Reporting dashboards: https://developer.productive.io/reference/download_spec?group=dashboards pulses: https://developer.productive.io/reference/download_spec?group=pulses report-category: https://developer.productive.io/reference/download_spec?group=report-category reports: https://developer.productive.io/reference/download_spec?group=reports widgets: https://developer.productive.io/reference/download_spec?group=widgets ### AI agent-configs: https://developer.productive.io/reference/download_spec?group=agent-configs agents: https://developer.productive.io/reference/download_spec?group=agents ## Conventions Format: JSON:API — body: {"data":{"type":"tasks","attributes":{},"relationships":{}}} Filter: ?filter[field]=value e.g. ?filter[assignee_id]=42 Paginate: ?page[number]=1&page[size]=30 (max 200) Sort: ?sort=name or ?sort=-name (descending) Rate: 100 req/10s per token, 4000 req/30min per org; reports: 10 req/30s; 429 "Too many requests" Budget: server processing time per org, independent of the counts above: 30min/hour, 6h/day; 429 "Server time limit exceeded" ## Create a task curl -X POST https://api.productive.io/api/v2/tasks \ -H "X-Auth-Token: TOKEN" -H "X-Organization-Id: ORG_ID" \ -H "Content-Type: application/vnd.api+json" \ -d '{"data":{"type":"tasks","attributes":{"title":"My task"},"relationships":{"task_list":{"data":{"type":"task_lists","id":"123"}}}}}' ## Log time (time in minutes; requires person_id + service_id) curl -X POST https://api.productive.io/api/v2/time_entries \ -H "X-Auth-Token: TOKEN" -H "X-Organization-Id: ORG_ID" \ -H "Content-Type: application/vnd.api+json" \ -d '{"data":{"type":"time_entries","attributes":{"date":"2024-01-15","time":60},"relationships":{"person":{"data":{"type":"people","id":"456"}},"service":{"data":{"type":"services","id":"789"}}}}}' Time entry fields: https://developer.productive.io/reference/download_spec?group=time-entries Changelog: https://developer.productive.io/reference/changelog ## Guides ### Overview URL: https://developer.productive.io/guides/overview API endpoint is https://api.productive.io/api/v2/ This API is implemented according to [JSON API spec](https://jsonapi.org/). ##### Exporting the Financial Item report URL: https://developer.productive.io/guides/exporting-the-financial-item-report The Financial Item report supports cursor pagination through the `page[after]` parameter. Use it to export the whole report. Every page costs the same regardless of how deep into the report it is, so the export does not get slower as it goes. `GET https://api.productive.io/api/v2/reports/financial_item_reports` Cursor pagination is opt-in per request. Without `page[after]` the report paginates with `page[number]` exactly as before. This guide covers what is specific to the Financial Item report. The general rules for cursor pagination are described in [Pagination](guide:pagination) NOTE: The Financial Item report is currently the only report that supports cursor pagination. Other report endpoints return `400 keyset_unsupported_sort` for `page[after]`. ## Requirements A request has to meet all of the conditions below. If it misses any of them, the request returns a `400` error naming the reason. It does not fall back to page-based pagination. 1. **Send `page[after]`, empty on the first request.** Its presence is what opts the request in. Without it you get page-based pagination. 2. **Do not send `page[number]`.** Sending both returns `keyset_conflict`. 3. **Filter on exactly one `financial_item_type`.** Exporting several types means one crawl per type. 4. **Do not send any other filter.** `filter[financial_item_type]` is the only filter accepted on this path. Date range, project and budget filters are not supported here, so narrow the rows on your side after the export. 5. **Do not send a `group` parameter.** The crawl runs at the financial item level: one row per underlying record. 6. **Do not send a `sort` parameter.** The cursor defines the order. 7. **Use a token whose user can view all budgets and all deals.** A user with restricted visibility gets `report_keyset_restricted_access` and has to use `page[number]` instead. `include` and sparse fieldsets (`fields[...]`) work as usual. ## Parameters | Parameter | Value | Notes | | --- | --- | --- | | `page[after]` | empty, then the cursor from the response | Required. Send it empty to start, after that you never set it by hand | | `page[size]` | `1`-`1000` | 30 by default. Values above the maximum are reduced to it, not rejected | | `filter[financial_item_type][eq][]` | one item type | Required, exactly one value | | `include` | comma-separated relationships | Optional. Has the largest effect on how long a page takes, so request only what you use | | `fields[...]` | sparse fieldsets | Optional | ## Requesting the first page ``` bash curl -sS -G 'https://api.productive.io/api/v2/reports/financial_item_reports' \ -H 'X-Auth-Token: '"$PRODUCTIVE_AUTH_TOKEN" \ -H 'X-Organization-Id: '"$PRODUCTIVE_ORGANIZATION_ID" \ -H 'Content-Type: application/vnd.api+json' \ --data-urlencode 'filter[financial_item_type][eq][]=time_entry' \ --data-urlencode 'page[after]=' \ --data-urlencode 'page[size]=1000' ``` ## Following the cursor The `links` section of each response contains a `next` URL. Request it as it is, and repeat until a response comes back without a `next` link. There is nothing to calculate on your side. You do not read an id off the last row or keep track of a position. The server does that and returns a complete URL with your filter and page size already carried over. Two things to keep in mind: - **A missing `links.next` is the only end signal.** A short page is not. A page can come back with fewer rows than `page[size]` and still be followed by more pages. - **The cursor is opaque.** It encodes a position and the order it was issued under. Do not construct, parse or increment it, send back exactly what you were given. The whole protocol, in pseudocode: ``` url = ".../financial_item_reports?filter[financial_item_type][eq][]=time_entry" + "&page[after]=&page[size]=1000" rows = [] while url: response = GET(url) if response.status == 429: # throttled, see Rate limits sleep(response.headers["X-RateLimit-Reset"]) continue # the same url, not the next one if response.status >= 500: # transient, see Retrying and resuming sleep(backoff()) continue # the same url, still safe rows += response.body["data"] url = response.body["links"].get("next") # absent means done ``` ## The response A cursor response differs from a page-based one in two places: | Field | On a cursor page | | --- | --- | | `meta.page_size` | Returned, the size that was applied | | `meta.max_page_size` | Returned, the maximum for this request | | `meta.total_count` | Not returned | | `meta.total_pages` | Not returned | | `links.first` | Returned, restarts the crawl | | `links.next` | Returned until the last page, then omitted | | `links.last`, `links.prev` | Not returned, the crawl only goes forward | The first page of a two page crawl, requested with `page[after]=&page[size]=2`: ``` json { "data": [ { "id": "...booking_item-534..." }, { "id": "...booking_item-536..." } ], "meta": { "page_size": 2, "max_page_size": 1000 }, "links": { "first": ".../financial_item_reports?filter[...]&page[after]=&page[size]=2", "next": ".../financial_item_reports?filter[...]&page[after]=eyJ2IjoxLCJzb3J0...&page[size]=2" } } ``` The second page, requested by following `links.next` unchanged. It has no `next` link, so the crawl is finished: ``` json { "data": [ { "id": "...booking_item-538..." } ], "meta": { "page_size": 2, "max_page_size": 1000 }, "links": { "first": ".../financial_item_reports?filter[...]&page[after]=&page[size]=2" } } ``` NOTE: Cursor pages do not contain `total_count`, so you cannot show progress as a percentage from them. To get a total, send one page-based request with the same type filter and `page[size]=1` first, and read `meta.total_count` from it. ## Page size The maximum `page[size]` on the cursor path is **1000**. Page-based requests stay capped at 200. Values above the maximum are reduced to it rather than rejected, so a request for 2000 returns 1000. Read `meta.page_size` if you need to know which value was applied. Use the largest page you can. Rate limits are counted per request and not per row, so halving the number of pages halves the time the export spends waiting on the limit. ## Item types Exactly one of these per crawl: | Value | Rows | | --- | --- | | `time_entry` | Time entries | | `booking_item` | Booking items | | `draft_booking_item` | Tentative booking items, bookings that are still drafts | | `service` | Services on budgets and deals | | `expense` | Expenses | | `invoice_attribution` | Invoice attributions | | `revenue_item` | Revenue items, only when revenue recognition is enabled on the account | A full export is therefore several crawls, run one after another. They share the same rate limit per API token, so running more than two or three of them in parallel does not help. A type with no matching records returns one empty page and no `links.next`. ## Possible errors A request that does not meet the requirements returns a `400` instead of the first page-based page. Branch on `errors[0].code`: | Code | Cause | Fix | | --- | --- | --- | | `keyset_conflict` | `page[after]` sent together with `page[number]` | Remove `page[number]` | | `report_keyset_requires_single_type` | No `financial_item_type` filter, or more than one value | Send exactly one value, one crawl per type | | `report_keyset_unsupported_filter` | A filter other than `financial_item_type` was sent | Remove it, or use `page[number]` | | `report_keyset_unsupported_group` | A `group` parameter was sent | Remove it | | `report_keyset_restricted_access` | The user cannot view all budgets and all deals | Use a token with full visibility, or use `page[number]` | | `keyset_unsupported_sort` | A `sort` was sent, or the endpoint does not support cursor pagination | Remove `sort` | | `keyset_invalid_cursor` | The cursor is malformed, constructed by hand, or was issued for a different item type | Restart that crawl from `page[after]=` | NOTE: A `sort` value that is invalid on its own returns a different code (`sort_param_unsupported`, `missing_date_period`), so handle any `400` rather than only the codes listed here. ## Rate limits Report endpoints allow **10 requests per 30 seconds per API token**. Keep the export under that and you will rarely see a `429`. There is no `Retry-After` header, use these instead: | Header | Meaning | | --- | --- | | `X-RateLimit-Limit` | Requests allowed in the window | | `X-RateLimit-Remaining` | `0` while you are being limited | | `X-RateLimit-Reset` | Seconds until the window resets, wait that long and retry the same URL | The general rules are described in [Rate Limits](guide:rate-limits) ## Retrying and resuming A cursor request is safe to repeat. Requesting the same `page[after]` again returns the same page, because the position is in the URL and not in server-side state. There is no session to lose and no page counter that can get out of step. Retry a `429` or a `5xx` against the same URL and the export continues where it stopped. An export that runs for several minutes will hit a transient `5xx` sooner or later, so build the retry in from the start. If your integration stores progress, store the last cursor you processed and resume from it. While an export is running: - The crawl moves forward in creation order. Records created during the export land ahead of the cursor and are included, records deleted during the export are not returned. - A cursor belongs to the export that produced it. Start every new export from `page[after]=` instead of reusing a cursor from a previous run. - Changing `page[size]` during an export is safe. Changing the item type is not, that cursor returns `keyset_invalid_cursor`. ## Complete example A full export in Python, using the standard library only. It crawls every item type in turn, keeps itself under the rate limit, retries limited and failed requests, and writes one JSON Lines file per type.
financial_item_export.py ```python #!/usr/bin/env python3 """Export the Productive Financial Item report using cursor pagination. Crawls every financial item type in turn and writes one JSON Lines file per type. Stays inside the reports rate limit (10 requests / 30 seconds) and retries throttled and failed requests safely. Usage: export PRODUCTIVE_AUTH_TOKEN="..." export PRODUCTIVE_ORGANIZATION_ID="..." python3 financial_item_export.py """ import json import os import sys import time import urllib.error import urllib.parse import urllib.request BASE_URL = os.environ.get( "PRODUCTIVE_API_URL", "https://api.productive.io/api/v2/reports/financial_item_reports" ) AUTH_TOKEN = os.environ["PRODUCTIVE_AUTH_TOKEN"] ORGANIZATION_ID = os.environ["PRODUCTIVE_ORGANIZATION_ID"] # One crawl per type. Drop any you do not need. ITEM_TYPES = [ "time_entry", "booking_item", "draft_booking_item", "revenue_item", "service", "invoice_attribution", "expense", ] PAGE_SIZE = 1000 # maximum accepted on the cursor path INCLUDE = "" # e.g. "service,person" — adds relationships, costs time REQUEST_TIMEOUT = 180 # seconds RATE_LIMIT = 10 # requests ... RATE_PERIOD = 30 # ... per this many seconds, per API token MAX_ATTEMPTS = 6 # per page, across throttling and server errors class RateLimiter: """Keeps the client under `limit` requests per rolling `period` seconds.""" def __init__(self, limit, period): self.limit = limit self.period = period self.sent = [] def wait(self): while True: now = time.monotonic() self.sent = [t for t in self.sent if now - t < self.period] if len(self.sent) < self.limit: self.sent.append(now) return time.sleep(self.period - (now - self.sent[0]) + 0.05) def fetch(url, limiter): """GET one page, honouring 429s and retrying transient server errors. Re-requesting the same URL is always safe: the cursor is in the URL, so a retry returns the same page rather than skipping or repeating rows. """ for attempt in range(MAX_ATTEMPTS): limiter.wait() request = urllib.request.Request( url, headers={ "X-Auth-Token": AUTH_TOKEN, "X-Organization-Id": ORGANIZATION_ID, "Content-Type": "application/vnd.api+json", }, ) try: with urllib.request.urlopen(request, timeout=REQUEST_TIMEOUT) as response: return json.loads(response.read().decode("utf-8")) except urllib.error.HTTPError as error: body = error.read().decode("utf-8", "replace") if error.code == 429: reset = int(error.headers.get("X-RateLimit-Reset") or RATE_PERIOD) print(f" throttled, waiting {reset}s", file=sys.stderr) time.sleep(reset + 1) continue if error.code >= 500: backoff = 2 ** attempt print(f" HTTP {error.code}, retrying in {backoff}s", file=sys.stderr) time.sleep(backoff) continue raise SystemExit(f"HTTP {error.code} for {url}\n{body}") except urllib.error.URLError as error: backoff = 2 ** attempt print(f" {error.reason}, retrying in {backoff}s", file=sys.stderr) time.sleep(backoff) raise SystemExit(f"gave up after {MAX_ATTEMPTS} attempts: {url}") def first_page_url(item_type): params = [ ("filter[financial_item_type][eq][]", item_type), ("page[after]", ""), ("page[size]", str(PAGE_SIZE)), ] if INCLUDE: params.append(("include", INCLUDE)) return f"{BASE_URL}?{urllib.parse.urlencode(params)}" def crawl(item_type, limiter): """Walk one type to exhaustion, writing rows to .jsonl.""" url = first_page_url(item_type) pages = 0 rows = 0 started = time.monotonic() with open(f"{item_type}.jsonl", "w", encoding="utf-8") as output: while url: body = fetch(url, limiter) for row in body["data"]: output.write(json.dumps(row, ensure_ascii=False) + "\n") pages += 1 rows += len(body["data"]) print(f" page {pages}: {len(body['data'])} rows ({rows} total)") # The only end signal. A short page is not one. url = body.get("links", {}).get("next") return pages, rows, time.monotonic() - started def main(): limiter = RateLimiter(RATE_LIMIT, RATE_PERIOD) total = 0 for item_type in ITEM_TYPES: print(f"{item_type}:") pages, rows, seconds = crawl(item_type, limiter) total += rows print(f" done — {rows} rows, {pages} pages, {seconds:.1f}s -> {item_type}.jsonl\n") print(f"{total} rows exported across {len(ITEM_TYPES)} types") if __name__ == "__main__": main() ```
Save it as `financial_item_export.py` and run: ``` bash export PRODUCTIVE_AUTH_TOKEN="..." export PRODUCTIVE_ORGANIZATION_ID="..." python3 financial_item_export.py ``` #### Authorization URL: https://developer.productive.io/guides/authorization To authorize yourself, add your API token to the **X-Auth-Token** header for every request. API token can be generated using Productive application, navigating to **Settings** -> **API integrations** -> **Generate new token** To access your organization data, add your organization ID to the **X-Organization-Id** header for every request. Most resources have authorization on them. If successfully authorized, you will get a response containing the resource. However, if you aren't authorized then you will be given HTTP status of 403, and an error message. ### Document Format URL: https://developer.productive.io/guides/document-format The Productive Document Format describes the elements supported in rich text fields like `pages.body`. It defines what you can write through the Markdown and HTML proxy endpoints documented in [Importing Docs via API](https://developer.productive.io/importing_docs_via_api.html). It's currently used in the following resources: | Resource | Property | Description | | -------- | -------- | ----------- | | `pages` | `body` | Productive Docs are represented by the `pages` resource and its `body` is written and read in this format | ### How content is sent The content shown in this guide goes into the `markdown` or `html` field of a proxy endpoint request. For example, to append the content to a page: Markdown path: ```json PATCH /api/v2/pages/:id/append_markdown { "markdown": "## Daily standup\n\n- Build is green" } ``` HTML path: ```json PATCH /api/v2/pages/:id/append_html { "html": "

Daily standup

" } ``` When creating a page, the field lives inside the standard JSON:API envelope alongside the other page attributes (`title`, `project_id`, …): ```json POST /api/v2/pages/create_with_markdown { "data": { "type": "pages", "attributes": { "title": "Sprint planning", "project_id": 12345, "markdown": "# Sprint 42\n\n- Ship the dashboard" } } } ``` For full request examples (headers, response shapes, append vs replace, error pitfalls), see [Importing Docs via API](https://developer.productive.io/importing_docs_via_api.html). The rest of this guide describes what you can put inside the `markdown` and `html` strings. ### Supported nodes and marks **Block elements:** `paragraph`, `blockquote`, `heading`, `codeblock`, `ol`, `ul`, `checklist`, `table`, `divider`, `banner`, `columns` **Inline elements:** `text`, `image`, `file`, `mention`, `var`, `br` **Marks (text formatting):** `link`, `em`, `strike`, `underline`, `strong`, `code`, `discussion`, `styles` ### Node types **Paragraph** Plain text separated by blank lines. The Markdown form is the default — no syntax. The HTML form supports optional `style="text-align: left|center|right|justify"`. ```html

Hello world.

Centered text.

``` **Blockquote** Markdown: `> Hello world` HTML: `

Hello world

` **Heading** Three levels are supported. Markdown: `#`, `##`, `###`. HTML: `

`, `

`, `

`. Higher levels (`####` / `

` and below) are not rendered. **Codeblock** A fenced code block with an optional language hint. The HTML form uses the `codeblock-language` attribute. ````markdown ```ruby puts 'hello' ``` ```` ```html
puts 'hello'
``` **Ordered list** Markdown: `1. item`. HTML: `
    ` of `
  1. ` items. The HTML form supports `
      ` to change the starting number. **Unordered list** Markdown: `- item`. HTML: `