Skip to content

Add Carta Issuer API connector - #59

Draft
fivetran-tommy wants to merge 1 commit into
fivetran:mainfrom
fivetran-tommy:ts/task/GA-1020864/carta-issuer-api-connector
Draft

Add Carta Issuer API connector#59
fivetran-tommy wants to merge 1 commit into
fivetran:mainfrom
fivetran-tommy:ts/task/GA-1020864/carta-issuer-api-connector

Conversation

@fivetran-tommy

Copy link
Copy Markdown

What this adds

A connector for the Carta Issuer API, which exposes the securities a company has issued. It replicates 16 tables: option grants, restricted stock units, restricted stock awards, certificates, flattened vesting events and option grant exercises, stakeholders, share classes, 409A fair market values and their per share class valuations, vesting schedule templates, convertible notes, stakeholder cap table holdings and their per share class breakdown, and issuer and corporation metadata.

Read only. No write calls to Carta.

The common use is total compensation reporting: stakeholders.employee_id and stakeholders.email join equity to an HRIS or payroll source, which no single Carta export does flexibly. Dilution analysis and vesting forecasts fall out of the same tables.

Why these design choices

Four things about this API are not obvious, and each one shaped the implementation. All four are written up in the connector README.

Scopes are granted all or nothing per OAuth application. If an application is not registered for one of the requested scopes, Carta returns a token with no scope at all, and that token is then rejected by every data endpoint. So the connector raises immediately on an empty granted scope, and it treats a 403 on a resource as a permanent fact about the application rather than a transient error: InsufficientScopeError is caught per resource and logged as a warning, so an application with a narrower grant replicates what it can reach instead of failing the sync. scopes is configurable for exactly this reason.

Tokens are short lived. The production lifetime is one hour, which a first sync can outlive. A 401 triggers one token refresh and one retry. A second consecutive 401 is raised so a genuinely bad credential fails fast.

The incremental cursor cannot be advanced mid-resource. Only four resources accept lastModifiedDatetimeAfter, and Carta does not return records in modified order, so a cursor written partway through could skip older records not yet fetched. The cursor is therefore written only when a resource completes. Mid-resource checkpoints persist delivered rows without moving it, and an interrupted sync safely refetches from the last completed cursor.

One security expands into many child rows. A single option grant can carry hundreds of vesting events, so the checkpoint interval is measured in upserts (10,000) rather than parent records. That keeps each commit small, which matters because a single very large commit at the end of a resource can fail and roll back everything already sent.

Two smaller notes: equity quantities and prices are declared STRING because Carta returns high-precision decimals such as 99.00000000000000000000 and a float would silently lose precision; and the stakeholder field Carta calls group lands as stakeholder_group, since group is reserved in most warehouses.

One connection can sync several issuers through a comma separated issuer_ids value. Every row carries its issuer_id and every cursor is namespaced by issuer.

Validation

black --line-length 99 and flake8 against the repository .flake8 config both pass on carta/.

The connector was exercised end to end offline, with requests and time.sleep stubbed and the Carta API routed by URL, so no credentials or network were involved. The run covered two issuers and every resource, and deliberately injected one expired token and one permanently scope denied resource:

upserts by table:
    certificates                                  2
    convertible_notes                             2
    corporations                                  1
    fair_market_value_share_class_valuations      2
    fair_market_values                            2
    issuers                                       2
    option_grant_exercises                        2
    option_grants                                 2
    restricted_stock_units                        2
    share_classes                                 2
    stakeholder_holdings                          2
    stakeholder_share_class_holdings              2
    stakeholders                                  2
    vesting_events                                6

checkpoints: 22   API GETs: 24   tokens minted: 2

What that run confirms:

  • The forced 401 caused exactly one token refresh, and the sync continued.
  • The forced 403 skipped only that resource, once per issuer, with a warning naming it. No cursor was written for it and its table received no rows.
  • Cursors landed per issuer and per resource, for example 111_optionGrants_last_modified and 222_certificates_last_modified.
  • Nested structures flattened as intended: 2 grants produced 4 vesting events and 2 exercises, and the RSUs produced 2 more vesting events.
  • corporations synced once for the whole sync rather than once per issuer.
  • High-precision quantities survived as strings, money objects split into amount and currency, and group landed as stakeholder_group.
  • Every table upserted is declared in schema().

An earlier revision of this description claimed 17 tables. The harness check against schema() caught it: the correct count is 16.

Known limitations

  • Warrants and interests are deliberately excluded. Neither endpoint was reachable during development (interests requires a UUID issuer identifier, which numeric issuer ids do not satisfy), so their column shapes could not be confirmed. Better to omit them than to ship tables with a guessed schema.
  • Compensation benchmarks and the capitalization table summary are also excluded. The scopes exist, but every documented path returned 404.
  • The issuers detail endpoint is not reachable for every application, so that table is best effort and is skipped with a warning when it 404s. corporations covers legal entity information in that case.
  • Hard deletes are invisible: the cursor reports modifications only and Carta publishes no deletions feed. Most equity deletions arrive as status changes such as canceled or forfeited, which are ordinary updates. A periodic re-sync is the workaround.

Syncs issuer-level equity from the Carta Issuer API: option grants, RSUs, RSAs,
certificates, flattened vesting events and exercises, stakeholders, share classes,
409A fair market values and their per share class valuations, vesting schedule
templates, convertible notes, stakeholder cap table holdings, and issuer and
corporation metadata. 16 tables.

One connection can cover several issuers through a comma separated issuer_ids
value, and every incremental cursor is namespaced by issuer.

Notable behavior, all documented in the README:

- Incremental sync for the four securities resources that accept Carta's
  lastModifiedDatetimeAfter cursor. The cursor is written only when a resource
  completes, because Carta does not return records in modified order.
- Equity quantities and prices are declared STRING so Carta's high-precision
  decimals do not lose precision.
- Carta grants OAuth scopes all or nothing per app, so a 403 is a permanent fact
  about the app rather than a transient error. It is raised as
  InsufficientScopeError, caught per resource, and logged, so an app with a
  narrower grant still replicates what it can reach.
- A 401 triggers one token refresh and one retry, so a first sync longer than the
  one hour production token lifetime completes.
- Checkpoints every 10,000 upserts inside a resource. One security can expand into
  hundreds of vesting events, and a single very large commit at the end of a
  resource can fail and roll back rows already sent.
@fivetran-tommy fivetran-tommy self-assigned this Aug 10, 2026
@cla-assistant

cla-assistant Bot commented Aug 10, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Carta Issuer API Connector SDK example connector to the community_connectors repo, intended to replicate issuer-level equity/securities data into 16 destination tables with incremental sync where supported.

Changes:

  • Adds the carta/ connector implementation, including OAuth2 token handling, pagination, state management, and schema definitions.
  • Adds connector-specific documentation (carta/README.md) and a starter carta/configuration.json.
  • Registers the new connector in the repository’s top-level README.md connector list.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.

File Description
README.md Adds Carta to the catalog/list of available community connectors.
carta/README.md Documents connector purpose, setup, configuration, and replicated tables.
carta/connector.py Implements the Carta Issuer API connector (schema + update, auth, pagination, sync logic).
carta/configuration.json Provides placeholder configuration keys required by the connector.
Suppressed comments (7)

carta/README.md:28

  • The fivetran init command should follow the pattern used across this repo (fivetran init --template <folder>). Using connectors/carta and <project-path> is inconsistent with other examples and may not work as intended.
fivetran init <project-path> --template connectors/carta

carta/README.md:31

  • The template README includes a short explanation paragraph about what fivetran init does, and notes should be formatted as > Note: (no space before the colon). Adding the missing paragraph here also makes the Getting started section consistent with other connectors.
> Note : Ensure you have updated the `configuration.json` file with the necessary parameters before running `fivetran debug`. See the [Configuration file](#configuration-file) section for details on the required configuration parameters.

carta/README.md:47

  • Use fenced code blocks with language hints for configuration examples (e.g., use json for the configuration snippet) to match the documentation guidelines for connector READMEs.
**carta/README.md:85**
* The Authentication section should include a numbered list of user actions describing how to obtain credentials and set up authentication (the current numbered list describes technical requirements, not user actions).

Carta uses OAuth2 with the client_credentials grant. Register an application in the Carta Developer Portal to obtain a client id and secret, and have Carta promote the application before using it against production data.

**carta/README.md:140**
* The Tables created section needs to include the list of columns (and primary key) for each table (for example by pasting the per-table schema objects from `schema()`), not just the table names and primary keys.

Tables created

Securities and their children:

  • option_grants (primary key issuer_id, id), incremental. Stock option grants.
**carta/connector.py:1410**
* The first log statement in `update()` should follow the required format `log.warning("Example: <CATEGORY> : <EXAMPLE_NAME>")` (note the ` : ` separator).
log.warning("Example: Source Examples - Carta Issuer API Connector")
**carta/connector.py:989**
* Per the repo’s Python connector guidelines, each `op.checkpoint()` call should be preceded by the standard checkpoint comment block (the current comment is abbreviated and doesn’t match the required wording).
# Save the progress by checkpointing the state once the resource and its cursor are complete.
op.checkpoint(state)
</details>



---

💡 <a href="/fivetran/community_connectors/new/main?filename=.github/skills/code-review/SKILL.md" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Add a `code-review` agent skill</a> or configure MCP servers for context-aware, tailored reviews. <a href="https://docs.github.com/en/copilot/how-tos/use-copilot-agents/request-a-code-review/use-code-review#mcp-servers-and-agent-skills" class="Link--inTextBlock" target="_blank" rel="noopener noreferrer">Learn more in the docs.</a>

Comment thread carta/README.md
Comment on lines +15 to +19
- [Supported Python versions](https://github.com/fivetran/connector_sdk/blob/main/README.md#requirements)
- Operating system:
- Windows: 10 or later (64-bit only)
- macOS: 13 (Ventura) or later (Apple Silicon [arm64] or Intel [x86_64])
- Linux: Distributions such as Ubuntu 20.04 or later, Debian 10 or later, or Amazon Linux 2 or later (arm64 or x86_64)
Comment thread carta/connector.py
Comment on lines +1400 to +1409
"""
Define the update function, which is a required function, and is called by Fivetran during
each sync.
See the technical reference documentation for more details on the update function
https://fivetran.com/docs/connector-sdk/technical-reference/connector-sdk-code/connector-sdk-methods#update
Args:
configuration: A dictionary containing connection details
state: A dictionary containing state information from previous runs
The state dictionary is empty for the first sync or for any full re-sync
"""
Comment thread carta/connector.py
Comment on lines +955 to +956
# The 'upsert' operation inserts or updates the record in the destination table.
op.upsert(table=table, data=row_builder(issuer_id, record))
Comment thread carta/connector.py
Comment on lines +568 to +570
wait_seconds = int(
response.headers.get("Retry-After", __DEFAULT_RETRY_AFTER_SECONDS)
)
@fivetran-sahilkhirwal

Copy link
Copy Markdown
Contributor

Hi @fivetran-tommy
Is this PR ready for review?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants