Skip to Content
ReferenceAuthentication & rolesOverview

Authentication, API keys and roles

plugsync has three credentials, and each one opens a different part of the API. This page says which credential to send to which endpoint, what an organization API key can and cannot do, how the owner and member roles split the work inside an organization, and how to manage the team and rotate keys.

CredentialLooks likeSent asOpensCreated by
Organization API keyps_org_...Authorization: Bearer ps_org_...The config plane (/api/...): connectors, drafts, publish, events, replayAn owner
User sessionA JWTAuthorization: Bearer <access_token>The config plane, plus the routes that need a personLogging in
Connector API keyps_live_...x-api-key: ps_live_...The event plane only: sending events to one connectorAny member, owner or organization key

The two planes do not mix. A connector key is refused on the config plane (401). The event plane reads only the x-api-key header: an organization key or a session token sent as Authorization: Bearer answers 401 Missing x-api-key header, and an organization key put in x-api-key answers 401 Invalid or revoked API key. Ingestion always takes a connector key.

Organization API keys

An organization API key is the credential for scripts, CI and the CLI. It is the same Authorization: Bearer token on every config-plane endpoint as a login session, with no 15-minute expiry, no refresh calls and no cookie jar.

Fields

FieldRequiredMeaning
labelyes1 to 100 characters. What the key is for (ci, nightly-export). It is the only thing that tells two keys apart in the listing, in GET /api/me and in the record of who started a drain or a data export, so make it specific.
expires_atnoISO 8601 datetime. Omit it for a key that never lapses. It must be in the future (422 otherwise); a value without a timezone, a bare date included, is read as UTC. Past that instant the key answers 401 exactly like a revoked key.

There is no scope field. A key is not restricted to a connector, to read-only calls or to a subset of endpoints: it acts on the whole organization, on every connector, with the authority described in What an organization key can do. Store it like a password, and give each automation its own key so you can revoke one without breaking the others.

Managing keys

All three calls need a signed-in owner (an organization key cannot mint, list or revoke keys, see Owner-only actions). The dashboard does the same from Settings → API Keys.

MethodPathEffect
POST/api/org-api-keysCreate a key. Body {"label": "ci", "expires_at": "2027-01-31T00:00:00Z"} (expires_at optional). Answers 201 with the key and its raw_key, shown once: only a hash is kept, so a lost key cannot be recovered, only replaced.
GET/api/org-api-keysList the active keys: id, key_prefix (the first 12 characters of the key), label, is_active, created_at, last_used_at, expires_at. Never the key itself. Revoked keys are left out.
DELETE/api/org-api-keys/{key_id}Revoke a key, immediately. Answers 204; requests carrying it answer 401 from then on. A key of another organization answers 404.
curl -s -X POST https://app.plugsync.com/api/org-api-keys \ -H "Authorization: Bearer $OWNER_JWT" -H 'content-type: application/json' \ -d '{"label": "ci"}' # -> {"id": "...", "key_prefix": "ps_org_3f9a1", "label": "ci", "is_active": true, # "created_at": "...", "last_used_at": null, "expires_at": null, # "raw_key": "ps_org_..."}

last_used_at moves on every request the key authenticates. A key belongs to the organization, not to the owner who created it: it keeps working when that owner leaves or is demoted, so revoking the keys someone handled is part of offboarding them.

To check which key a script holds, call GET /api/me with it: the response has principal_type: "api_key", the org_id, the api_key_label and role: "api_key", and no user fields.

What an organization key can do

A key carries the fixed role api_key. It is neither an owner nor a member, so every route decides explicitly whether it admits one:

What a key cannot do:

  • Owner-only actions (team, invitations, ownership, organization settings, API keys, HubSpot connect links, billing details): 403 with {"detail": {"error": "owner_required"}}, or user_auth_required on the billing routes. The full list is in Owner-only actions.
  • Actions that need a person, listed in Actions that need a signed-in user: releasing a connector held for review, checkout and plan changes, creating an organization, creating a test-mode connector, accepting the legal documents.
  • The event plane: send events with a connector key.

User sessions

A user session is what the dashboard uses, and what a script gets from logging in with an email and a password. It is meant for interactive use: for anything unattended, use an organization key.

StepCallResult
Log inPOST /api/auth/login with {"email", "password"}, optional active_org_idaccess_token (a JWT) and expires_in (900 seconds, 15 minutes), plus an httpOnly refresh cookie and a readable CSRF cookie.
Second factorPOST /api/auth/mfa/challenge with {"mfa_token", "code"}Only when the account has two-factor authentication on: login then answers {"mfa_required": true, "mfa_token": "...", "expires_in": ...} instead of a session, and the session starts here, with a code from the authenticator app or a recovery code.
RefreshPOST /api/auth/refresh with the cookies and an X-CSRF-Token header equal to the CSRF cookieA new access_token. The refresh cookie is replaced on every call and lasts 14 days from the last refresh.
Log outPOST /api/auth/logout (same CSRF header), ?all=true to end every session of the accountThe refresh cookie stops working.

Once expires_in runs out, config-plane requests answer 401: refresh before it does. A 401 on refresh means the session is over (expired, logged out, or a refresh cookie presented twice, which ends the whole session family as a precaution): log in again. The API-first quickstart has the full curl sequence with the cookie jar.

Changing the password (POST /api/me/password) revokes the refresh cookies of every other session of the account and keeps the current one: the other sessions cannot refresh any more, but an access token they already hold keeps working until its expires_in runs out (at most 15 minutes).

Signing in with Google

When Google sign-in is available (GET /api/v1/auth/google/config answers {"enabled": true}), the dashboard’s login, signup and HubSpot install pages offer Continue with Google. It is a browser flow, not something to script: it ends in the same session as a password login (access token, refresh cookie, two-factor step when the account has it on). Which account it opens, creates or links, and when it refuses, is described step by step in Getting started: continue with Google; the codes it reports are in Google sign-in errors.

An account created with Google has no password: changing the password or the email, turning two-factor off and closing the organization all ask for it, and answer 401 wrong_password until one is set with the password reset (Forgot password? on the login page). The reset also unlinks Google from the account, and so does any password reset; the next Continue with Google links it again when Google is authoritative for the address.

Active organization

A user can belong to several organizations, with a different role in each. A session acts on one at a time, its active organization: the active_org_id returned by login and refresh. Every config-plane call with that token reads and writes that organization only.

To switch, call POST /api/me/active-organization with {"org_id": "..."}: it answers {"access_token": "...", "active_org_id": "..."}, a token for the new organization, and 403 not_a_member if the user does not belong to it. POST /api/auth/refresh also accepts {"active_org_id": "..."}, but it does not fail on an organization the user does not belong to: it silently falls back to another of their memberships, so check active_org_id in the response. Use POST /api/me/active-organization when a wrong organization must be an error. GET /api/me lists every membership with its role.

The role is read from the membership on every request, not stored in the token: promoting, demoting or removing someone takes effect on their next call, without waiting for their token to expire.

Connector API keys

A connector key authenticates the Event API: it goes in the x-api-key header and is bound to exactly one connector. It cannot read or change configuration.

MethodPathEffect
POST/api/connectors/{connector_id}/api-keysCreate a key. Body {"label": "ingest"} (1 to 100 characters, surrounding spaces trimmed). Answers 201 with raw_key, shown once.
GET/api/connectors/{connector_id}/api-keysList the active keys: id, key_prefix, label, is_active, created_at, last_used_at. Revoked keys are left out.
DELETE/api/connectors/{connector_id}/api-keys/{key_id}Revoke a key, immediately. Events sent with it answer 401 from then on.

These calls authenticate on the config plane: any member, owner or organization key of the connector’s organization can make them. A connector of another organization answers 404. In the dashboard: connector → Settings → API keys. A key can also be minted together with the connector, with "create_api_key": true and an optional "api_key_label" on POST /api/connectors or on creation from a template.

A connector key has no expiry date: it works until revoked. key_prefix is ps_live_ on every connector key, so the label is what tells them apart.

Roles: owner and member

Inside an organization every user is either an owner or a member. There is no other role, and no “admin”: the authority above a member is the owner.

  • owner: whoever creates the organization at signup, and anyone an owner invites or promotes as owner. An organization can have several owners, and always has at least one.
  • member: everyone else. A member works on connectors like an owner: config, drafts, publish, events, replay, connector keys. What a member cannot do is administer the organization, nor pull the operational levers listed in Owner or organization key: drain, identity mapping repair, identity pre-warm, data export, importing and revoking compat keys.

Organization API keys are not users and have no role in this sense: see What an organization key can do.

Owner-only actions

These routes need a signed-in owner of the organization. A member gets 403 with {"detail": {"error": "owner_required"}}, and so does an organization key (on the billing routes a key gets 403 user_auth_required instead).

ActionEndpoints
See and manage the team: list members, change a role, remove someoneGET /api/orgs/{org_id}/members, PATCH and DELETE /api/orgs/{org_id}/members/{user_id}
Invite people, list and revoke pending invitationsGET, POST /api/orgs/{org_id}/invitations, DELETE /api/orgs/{org_id}/invitations/{invitation_id}
Transfer ownershipPOST /api/orgs/{org_id}/members/transfer-ownership
Change the organization settings: name, incident notifications, record retentionPATCH /api/orgs/{org_id}
Create, list and revoke organization API keys/api/org-api-keys
Create, list and revoke HubSpot connect links, which let a HubSpot admin without a plugsync account connect a portal/api/orgs/{org_id}/hubspot-connect-links
Let a connector held for review after its first sync continuePOST /api/connectors/{connector_id}/write-cap/release
Edit the billing details, read order confirmations and the agreementPATCH /api/v1/billing/details, GET /api/v1/billing/order-confirmations, GET /api/v1/billing/agreement
Close the organization, or cancel a closurePOST, DELETE /api/orgs/{org_id}/closure

The routes under /api/orgs/{org_id}/... also check that {org_id} is the session’s active organization: an owner of two organizations switches before acting on the other one.

Release after the first sync. Some organizations have a first-sync review: a new connector pauses after its first records are written to HubSpot, and the owners get an email asking them to check those records in the CRM. The release resumes the connector and sends the held records on; it answers 409 write_cap_not_tripped when the connector is not held. It needs an owner signed in: an organization key is refused even though it can pull the other owner levers, because the check is meant for a person and the key is held by the same automation that sent the records. What happens before and after: First-sync write cap.

A connect link lets someone who administers a HubSpot portal, but has no plugsync account, connect that portal to your organization. An owner creates it, signed in (an organization key cannot), and sends it; the dashboard does the same from the “HubSpot” card on the Connectors page, as shown in Getting started.

MethodPathEffect
POST/api/orgs/{org_id}/hubspot-connect-linksCreate a link. No body. Answers 201 with id, url, expires_at, created_at. The url is shown once: the list does not carry it.
GET/api/orgs/{org_id}/hubspot-connect-linksList the organization’s links: id, created_at, expires_at, used_at, revoked_at, and once used credential_id, portal_id, hub_domain of the portal it connected.
DELETE/api/orgs/{org_id}/hubspot-connect-links/{link_id}Revoke a link that was not used yet. Answers 204, also for a link already revoked; 409 link_already_used for a used one, 404 link_not_found for an unknown one.

Rules of a link:

  • It expires 24 hours after it is created.
  • It connects one portal, once. It is spent when the connection succeeds: an attempt that fails or is cancelled on HubSpot can be retried with the same link while it is valid.
  • The connection is the organization’s, as if an owner had connected the portal: the plan’s portal limit applies, and approving a portal that is already connected to the organization updates its connection.

The person who opens the url sees a Connect HubSpot page naming your organization, with the scopes plugsync asks for and the link’s expiry; the button takes them to HubSpot’s consent screen and back to “HubSpot connected successfully.” A link that cannot be used answers, on the page and on its public calls, 410 with link_revoked, link_used or link_expired, or link_invalid for a link that is malformed (400) or unknown (404). The fix is a new link from an owner. All codes: API error catalog.

Owner or organization key

These levers are refused to members (403 owner_required) and accepted from an owner or an organization key:

ActionReference
Start and stop a drainDiscarding a backlog: drain
Read, remove or clear the tombstone of one identity mappingRepairing one identity mapping
Start, follow, resume and cancel an identity pre-warmPre-warming the identity map
Request and download a data exportData export
Import and revoke compat keys (listing them is open to members)Compat keys

Actions that need a signed-in user

These routes refuse an organization key whatever the role, because they act on behalf of a person:

ActionEndpointError for a key
Release a connector held for review (owner)POST /api/connectors/{connector_id}/write-cap/release403 owner_required
Start a checkout, preview a plan change, open the billing portalPOST /api/v1/billing/checkout-session, /plan-change-preview, /portal-session403 user_auth_required
Accept the updated legal documents (owner)POST /api/legal-acceptances403 user_session_required
Create another organization (the caller becomes its owner)POST /api/orgs403 api_key_cannot_create_org
Create a test-mode connectorPOST /api/connectors with "mode": "test"403 api_key_cannot_create_test_connector
Change your own profile, password, email or two-factor settings, or switch the active organizationPATCH /api/me, POST /api/me/active-organization, /password, /email, /email/confirm, every route under /api/me/mfa/403 user_auth_required: these act on a user account, and a key has none. GET /api/me accepts a key and describes it.

Managing the team

Invitations

An owner invites someone by email, choosing the role they join with:

curl -s -X POST https://app.plugsync.com/api/orgs/$ORG_ID/invitations \ -H "Authorization: Bearer $OWNER_JWT" -H 'content-type: application/json' \ -d '{"email": "ana@company.com", "role": "member"}' # -> 201 {"id": "...", "email": "ana@company.com", "role": "member", # "expires_at": "...", "created_at": "..."}

role is owner or member. The invitee gets an email with a link valid for 7 days. The call answers:

Statusdetail.errorWhen
409already_memberThe address already belongs to a member.
409invitation_pendingAn invitation to that address is still open.
402seat_limit_reachedThe plan’s seat limit is reached. Seats count members and open invitations (an expired invitation does not count); the body carries limit, usage and an upgrade_url. Accepting an invitation answers the same code only when the plan no longer has room for it, for example after a downgrade. See Team seats.

GET /api/orgs/{org_id}/invitations lists the open invitations, and DELETE /api/orgs/{org_id}/invitations/{invitation_id} revokes one (204, also when it was already revoked; 409 already_accepted once accepted). An invitation whose link has expired is no longer open: it leaves the list, frees its seat and does not block a new invitation to the same address.

The link opens the dashboard, where the invitee accepts. Someone without an account sets a name and a password there; someone who already has an account must sign in with the invited address first (the same link cannot add a different account). An accepted invitation signs the invitee in, with the new organization active.

Changing a role and removing someone

PATCH /api/orgs/{org_id}/members/{user_id} with {"role": "owner"} or {"role": "member"} changes a role; DELETE on the same path removes the member. Both answer 404 membership_not_found for a user who is not a member, and 409 last_owner when they would leave the organization without an owner. A removed user’s requests on the organization answer 403 from their next call, even with an access token issued before the removal. If it was their only organization, their refresh cookies are revoked as well, so no session of theirs can be renewed.

Transferring ownership

curl -s -X POST https://app.plugsync.com/api/orgs/$ORG_ID/members/transfer-ownership \ -H "Authorization: Bearer $OWNER_JWT" -H 'content-type: application/json' \ -d '{"to_user_id": "..."}' # -> {"new_owner": "..."}

The target becomes owner and the caller becomes member, in one step. The target must already be a member of the organization (404 target_not_member otherwise): invite them first. To add an owner without stepping down, promote them with PATCH .../members/{user_id} instead. The dashboard offers both from Settings → Team.

Rotating and revoking keys

Both kinds of key can coexist with others of the same kind, which is what makes a rotation free of downtime:

  1. Create the new key, with a label that tells it from the old one.
  2. Deploy it wherever the old one is used.
  3. Watch the old key’s last_used_at in the listing: once it stops moving, nothing uses it any more.
  4. Revoke the old key.

For an organization key you can also set expires_at when you create it, so that a forgotten key lapses on its own; plan the rotation before that date, since past it the key answers 401 with no warning. Connector keys have no expiry date.

Revocation is immediate and final: there is no call to reactivate a key. Revoke at once a key that may have leaked, then create its replacement.

Organization lifecycle

An organization lives until an owner closes it. Closing is not immediate: it starts a 30-day recovery period, fixed, during which the organization is stopped but nothing is deleted yet and any owner can change their mind. At the end of the period the organization and all its data are deleted, and that cannot be undone.

Closing the organization

In the dashboard: Settings → Organization → Close organization, a card shown to owners only. The dialog asks you to type the organization’s name, your password, and the authentication code when two-factor authentication is on for your account. An account created with Google has no password: set one first with Forgot password? on the login page. The reset signs out every session of the account and unlinks Google; the next Continue with Google links it again under the usual rules.

Through the API, a signed-in owner of the active organization calls:

curl -X POST "$PLUGSYNC_URL/api/orgs/$ORG_ID/closure" \ -H "Authorization: Bearer $ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{"confirm_name": "Acme", "password": "...", "mfa_code": "123456"}' # -> {"closing": true, "requested_at": "...", "deletes_at": "..."}

deletes_at is 30 days after requested_at. mfa_code is needed only with two-factor on. A member and an organization key get 403 owner_required. The request is refused when:

StatusCodeWhy
422confirmation_mismatchconfirm_name is not exactly the organization’s name (spaces around it are ignored).
401wrong_password, mfa_code_required, invalid_codeThe password is wrong (an account created with Google has none until it sets one), or the two-factor code is missing or wrong.
429Too many password change attemptsToo many wrong passwords or codes: the limit is shared with password changes.
409org_already_closingA closure is already running.
409org_has_commercial_agreementThe organization has a commercial agreement: it cannot close itself. Contact support@plugsync.com to end the agreement first.

While the organization is closing

From the moment of the request:

  • Every sync stops, inbound and outbound. The Event API, bulk imports, inbound webhooks, manual syncs, full extracts and replays answer 423 org_closing (Suspended or closing organization), and a HubSpot app installed from the marketplace cannot be attached to the organization.
  • Every member sees a banner, “This organization is being closed”, with the deletion date, and can still sign in and read the dashboard.
  • Owners can still export the data until the deletion date: see Data export.
  • Every owner gets an email, “Organization will be deleted on date”, listing what will be deleted.

Cancelling a closure

Any owner, signed in, can cancel until the deletion date: Cancel closure on the same card, or

curl -X DELETE "$PLUGSYNC_URL/api/orgs/$ORG_ID/closure" \ -H "Authorization: Bearer $ACCESS_TOKEN" # -> {"closing": false, "requested_at": null, "deletes_at": null}

No password is asked. Syncs resume and every owner gets an email that the closure was cancelled. What changed in HubSpot or in your systems while the organization was closing is not replayed automatically. Cancelling answers 409 org_not_closing when there is no closure, and 409 org_erasure_started once the deletion date has passed: from then on the deletion can no longer be stopped.

Deletion

Shortly after the deletion date the organization is deleted with everything in it: connectors, configuration and versions, credentials and HubSpot connections, identity mappings, event history, archived records, plugins, API keys, invitations and exports. The accounts of members who belong to no other organization are deleted too; members of other organizations keep their account. A paid subscription is cancelled.

Nothing is deleted in HubSpot: records plugsync wrote stay there, and the plugsync app stays installed on the portal until you uninstall it from HubSpot.

Every owner gets a last email when the deletion is done. As that email says, backups kept for disaster recovery expire on their own within 35 days, and invoices already issued are kept as the law requires.

Errors

The errors below are the ones specific to authentication and roles. The API error catalog lists every error code of the public API, with the field that carries it and the remedy.

StatusBodyMeaning
401Invalid or revoked API keyThe key is unknown, revoked or, for an organization key, past its expires_at. On the event plane, also an organization key sent in x-api-key.
401Missing x-api-key headerAn event-plane call without the x-api-key header, for example with a key or token sent as Authorization: Bearer.
401Invalid tokenThe session token is expired or malformed, or a connector key was sent as Bearer on the config plane.
403owner_requiredThe route needs an owner and the caller is a member or an organization key.
403wrong_active_orgA route under /api/orgs/{org_id}/... was called for an organization that is not the caller’s active one: switch the active organization first.
403user_auth_required, user_session_requiredThe route needs a signed-in user and was called with an organization key.
403User is not a member of the requested organizationThe session names an organization the user no longer belongs to: log in again.
409last_ownerThe change would leave the organization without an owner.
Last updated on