Download OpenAPI specification:Download
REST API for Zapa Client Portals management. See the release notes for what changed in each version.
All API endpoints require OAuth 2.0 authentication. Include your access token in the Authorization header:
Authorization: Bearer YOUR_ACCESS_TOKEN
Access tokens expire after 1 hour. Refresh tokens rotate: every refresh response returns a new
refresh_token, and the previous one stops working about 60 seconds later. Clients created in
Settings → API Settings must send client_secret when refreshing.
A portal can carry one identifier per external system (externalSystemIds, e.g.
{ "QuickBooks": "cust_12345" }). Set them with PATCH /api/v1/portals/{portalId} (merge-patch;
null removes a key), look portals up with GET /api/v1/portals?externalSystem=…&externalId=…,
and receive them on every webhook payload as external_system_ids. Pairs are unique per organization.
The taxi system is special: when the organization has connected the Taxi integration, setting
externalSystemIds.taxi to a 6-digit Taxi customer id links the portal and Zapa pushes uploads and
signed documents to that customer. The verification outcome is exposed read-only as taxi_link.
Subscribe to real-time events by registering webhooks. All webhook payloads are signed with HMAC-SHA256
over the raw request body (X-Webhook-Signature). Payloads are flat JSON: event, org_id, portal_id,
portal_name, external_system_ids, timestamp, plus event-specific fields such as file_id,
task_id, guest_email or workflow_state_name. See the
Developer Quickstart for verification code and
an example payload.
API resources can be turned into web links that open the app directly:
https://app.zapaportal.com/org/{org_id}/vault/{portal_id}/folder/main?file={file_id} to a portal link to open that file's preview automaticallyThe org_id, portal id, and file id values all come from the API responses below.
The API intentionally provides file metadata only — there is no endpoint that returns file contents or a download URL. Portals typically hold sensitive client documents (tax records, contracts, signed agreements), so downloads are restricted to signed-in users in the web app, where access is permission-checked and recorded in the portal's audit history. Every OAuth consent screen makes this promise to the end user: "This app will not have access to download file contents."
To send someone to a file, use a file preview link (above) rather than trying to fetch the bytes.
If your integration needs a capability this API doesn't offer yet, contact support@zapaportal.com — we prioritize API additions based on customer requests.
Exchanges an authorization code for an access token, or refreshes an expired access token.
This endpoint lives on the API domain (https://api.zapaportal.com), unlike the authorize
endpoint which is on the web app domain.
Access tokens expire after 1 hour (expires_in: 3600); store the refresh_token and use the
refresh_token grant to get a new one without user interaction. The endpoint accepts both
application/x-www-form-urlencoded and application/json bodies.
| grant_type required | string Value: "authorization_code" |
| code required | string |
| client_id required | string |
| client_secret required | string |
| redirect_uri required | string |
{- "grant_type": "refresh_token",
- "refresh_token": "string",
- "client_id": "string",
- "client_secret": "string"
}{- "access_token": "string",
- "token_type": "Bearer",
- "expires_in": 3600,
- "refresh_token": "string",
- "scope": "string"
}Returns the user and organization associated with the access token — user and org IDs and names, the OAuth client ID, and the scopes that were actually granted.
Call this first after obtaining a token: it verifies authentication works, tells you which scopes
the user approved on the consent screen (they may have granted fewer than you requested), and gives
you the org_id needed to build web links to portals and files. No specific scope is required —
any valid token works.
{- "user_id": "string",
- "user_name": "string",
- "user_email": "string",
- "org_id": "string",
- "org_name": "string",
- "client_id": "string",
- "scopes": [
- "string"
]
}Returns the organization's configured workflow states as {id, name} pairs. Call this before
setting a portal's workflow state — the workflow endpoint requires both values, and they must match
an existing state. Also handy for populating dropdowns in your integration's UI.
[- {
- "id": "string",
- "name": "string"
}
]Returns all active (non-deleted) portals in your organization, including tags, workflow state, task counters, and recent-activity timestamps — useful for syncing portal lists into a CRM or dashboard.
Templates: pass templatesOnly=true to list your organization's portal templates instead of
regular portals. Use a template's id as the templateId when creating a new portal to copy its
folder structure and settings.
Linking: build a web link to any portal with
https://app.zapaportal.com/org/{org_id}/vault/{id}/folder/main.
External-id lookup: pass both externalSystem and externalId to return only the portal(s)
whose externalSystemIds[externalSystem] equals externalId — e.g.
?externalSystem=taxi&externalId=cust_12345. Returns an empty array when nothing matches.
| templatesOnly | boolean Default: false When true, returns portal templates instead of regular portals |
| externalSystem | string External system name (as defined in Settings → External Systems). Must be paired with |
| externalId | string Identifier in the external system. Must be paired with |
[- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",
- "tags": [
- "string"
], - "workflowStateId": "string",
- "workflowStateName": "string",
- "archived": true,
- "externalSystemIds": {
- "property1": "string",
- "property2": "string"
}, - "taxi_link": {
- "status": "linked",
- "tenant_id": "string",
- "customer_id": "000123",
- "display_name": "string",
- "verified_at": "2019-08-24T14:15:22Z",
- "error": "string"
}, - "created_at": "2019-08-24T14:15:22Z",
- "created_by": "string"
}
]Creates a new client portal — typically one per client or engagement.
Creating from a template (Business & Enterprise): first call GET /api/v1/portals?templatesOnly=true
to find your template's id, then pass it as templateId. The new portal is created with the
template's folder structure and default settings:
{ "name": "ABC Corp - 2026 Tax Return", "templateId": "TEMPLATE_UUID", "tags": ["tax-2026"] }
Set isTemplate: true instead to create a new template rather than a regular portal.
Next steps: invite the client with POST /api/v1/portals/{portalId}/guests, then upload files
with POST /api/v1/portals/{portalId}/files. Subscribing a webhook to portal.created lets other
systems react to portals created here or in the web app.
| name required | string Display name, e.g. client name or project |
| tags | Array of strings Tags for filtering and grouping on the dashboard |
| isTemplate | boolean Create a reusable template instead of a regular portal |
| templateId | string <uuid> ID of a portal template to copy structure and settings from |
{- "name": "string",
- "tags": [
- "string"
], - "isTemplate": true,
- "templateId": "196100ac-4eec-4fb6-a7f7-86c8b584771d"
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",
- "tags": [
- "string"
], - "workflowStateId": "string",
- "workflowStateName": "string",
- "archived": true,
- "externalSystemIds": {
- "property1": "string",
- "property2": "string"
}, - "taxi_link": {
- "status": "linked",
- "tenant_id": "string",
- "customer_id": "000123",
- "display_name": "string",
- "verified_at": "2019-08-24T14:15:22Z",
- "error": "string"
}, - "created_at": "2019-08-24T14:15:22Z",
- "created_by": "string"
}Returns a single portal's details, including tags, workflow state, and activity timestamps. Use this to refresh one portal's state instead of re-listing everything, or to resolve a portal ID received in a webhook payload.
| portalId required | string <uuid> |
{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",
- "tags": [
- "string"
], - "workflowStateId": "string",
- "workflowStateName": "string",
- "archived": true,
- "externalSystemIds": {
- "property1": "string",
- "property2": "string"
}, - "taxi_link": {
- "status": "linked",
- "tenant_id": "string",
- "customer_id": "000123",
- "display_name": "string",
- "verified_at": "2019-08-24T14:15:22Z",
- "error": "string"
}, - "created_at": "2019-08-24T14:15:22Z",
- "created_by": "string"
}Updates a portal's name, tags, workflow state, or archive status. Only include the fields you want to change.
Set archived: true to archive a portal when an engagement ends — nothing is deleted, and it can
be restored later with archived: false. There is no delete endpoint; portals can only be
permanently deleted from the web app.
External system ids: externalSystemIds is a merge-patch — keys you send are set, a null
value deletes that key, and keys you omit are left unchanged. Each (system, id) pair must be
unique within your organization; a duplicate returns 409 conflict. The response echoes the
merged map.
| portalId required | string <uuid> |
| name | string |
| tags | Array of strings |
| workflowStateId | string |
| workflowStateName | string |
| archived | boolean |
object Merge-patch of external system ids. Values are strings (max 256 chars) or |
{- "name": "string",
- "tags": [
- "string"
], - "workflowStateId": "string",
- "workflowStateName": "string",
- "archived": true,
- "externalSystemIds": {
- "taxi": "cust_12345"
}
}{- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",
- "tags": [
- "string"
], - "workflowStateId": "string",
- "workflowStateName": "string",
- "archived": true,
- "externalSystemIds": {
- "property1": "string",
- "property2": "string"
}, - "taxi_link": {
- "status": "linked",
- "tenant_id": "string",
- "customer_id": "000123",
- "display_name": "string",
- "verified_at": "2019-08-24T14:15:22Z",
- "error": "string"
}, - "created_at": "2019-08-24T14:15:22Z",
- "created_by": "string"
}Moves a portal to a different workflow state (e.g. "In Progress" → "Review"). Fetch valid
workflowStateId/workflowStateName pairs from GET /api/v1/workflow-states first — both values
are required and should come from that list.
State changes fire the portal.workflow_changed webhook event, making this a good integration
point for driving external processes (e.g. notify your practice-management system when a portal
reaches "Complete").
| portalId required | string <uuid> |
| workflowStateId required | string |
| workflowStateName required | string |
{- "workflowStateId": "string",
- "workflowStateName": "string"
}Returns metadata (name, size, extension, upload date/user) for every active file and folder in a
portal. Folders appear as entries too — a file's parent_folder_file_id links it to its folder.
Linking to a file: to send someone to a specific file, build a web preview link:
https://app.zapaportal.com/org/{org_id}/vault/{portalId}/folder/main?file={file_id} — opening it
signs the user in (if needed) and opens that file's preview automatically.
Why no download endpoint? The API deliberately never returns file contents. Portal documents are often sensitive (tax records, contracts, signed agreements), and the OAuth consent screen promises users that connected apps cannot download their file contents. Downloads happen in the web app, where they're permission-checked per user and recorded in the portal's audit history. If your integration has a use case that requires content access, contact support@zapaportal.com to discuss it.
| portalId required | string <uuid> |
[- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "vault_id": "432b199b-1f71-42bf-ba0b-33d512afa9de",
- "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",
- "size": 0,
- "extension": "string",
- "parent_folder_file_id": "0a617e6d-dd07-4f15-a106-b7abf547121c",
- "uploaded_at": "2019-08-24T14:15:22Z",
- "uploaded_by": "string"
}
]Uploads a file to a portal. Two modes are supported:
One-step (recommended for automation): include a public HTTPS file_url and the server fetches
the file (up to 100 MB) and stores it immediately — the response is the completed file record.
This is how the Zapier integration uploads files.
Two-step (for uploading local bytes): omit file_url and the response contains a presigned
upload_url valid for 1 hour. Then:
PUT the raw file bytes to upload_url, sending the same Content-Type you specified here
(the response echoes the exact method and headers to use)POST /api/v1/portals/{portalId}/files/complete with the returned file_id to register
the file — until you do, the file will not appear in the portalSuccessful uploads fire the file.uploaded webhook event and notify portal users per their
notification settings. Pass parent_folder_file_id (a folder's file ID from the list endpoint) to
place the file inside a folder.
| portalId required | string <uuid> |
| filename required | string File name including extension, e.g. |
| contentType | string MIME type (defaults to |
| size | integer File size in bytes (used for the two-step flow's file record) |
| file_url | string <uri> Public HTTPS URL to fetch the file from (one-step mode, max 100 MB) |
| parent_folder_file_id | string <uuid> Folder to place the file in (omit for the portal root) |
| task_id | string <uuid> Attach the uploaded file to this task in the same portal. Uploads made with an API token are attributed to a member, so the task is linked but not auto-completed; guest uploads in the web app against an open File Request are marked Done. |
{- "filename": "string",
- "contentType": "string",
- "size": 0,
- "parent_folder_file_id": "0a617e6d-dd07-4f15-a106-b7abf547121c",
- "task_id": "736fde4d-9029-4915-8189-01353d6982cb"
}{- "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
- "expires_in": 3600,
- "method": "PUT",
- "headers": { }
}Registers a file after you've PUT its bytes to a presigned upload_url. This creates the file
record so it appears in the portal, notifies users, and fires the file.uploaded webhook event.
Only needed for the two-step upload flow — one-step (file_url) uploads are registered
automatically.
| portalId required | string <uuid> |
| file_id required | string <uuid> The |
| filename required | string |
| extension | string Defaults to the filename's extension |
| size | integer File size in bytes |
| parent_folder_file_id | string <uuid> |
| task_id | string <uuid> Attach the file to this task in the same portal (see upload endpoint for semantics) |
{- "file_id": "8a0cfb4f-ddc9-436d-91bb-75133c583767",
- "filename": "string",
- "extension": "string",
- "size": 0,
- "parent_folder_file_id": "0a617e6d-dd07-4f15-a106-b7abf547121c",
- "task_id": "736fde4d-9029-4915-8189-01353d6982cb"
}{- "error": "string",
- "error_description": "string"
}Returns all active tasks in a portal with their status, due date, assignees, and type. Useful for syncing outstanding client action items into an external task tracker, or checking whether a client has completed their requested items before advancing the portal's workflow state.
| portalId required | string <uuid> |
[- {
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "vault_id": "432b199b-1f71-42bf-ba0b-33d512afa9de",
- "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",
- "title": "string",
- "description": "string",
- "action_type": "General",
- "status": "Open",
- "due_date": "2019-08-24T14:15:22Z",
- "created_at": "2019-08-24T14:15:22Z"
}
]Creates a task in a portal — a request for the client (or your team) to do something, like
"Upload your W-2" or "Review the engagement letter". Tasks are created with status Open.
action_type categorizes the request (Question?, File Request, Sign Document, Fill Form,
or General). Creating a task fires the task.created webhook event.
Assignees: assigned_guest_ids must be user ids of guests already invited to the portal;
assigned_member_ids must be user ids of members of your organization. Display names are
resolved server-side. Unknown ids return 400.
Reminders: set send_reminder: true (requires due_date) to have Zapa email the portal's
guests on its standard reminder cadence (14, 7, 3 and 0 days before due, and 7 days after).
| portalId required | string <uuid> |
| title required | string |
| description | string |
| due_date | string <date-time> |
| action_type | string Default: "General" |
| assigned_guest_ids | Array of strings <uuid> [ items <uuid > ] User ids of portal guests to assign. Names are resolved server-side. |
| assigned_member_ids | Array of strings <uuid> [ items <uuid > ] User ids of organization members to assign. Names are resolved server-side. |
| send_reminder | boolean Default: false Email reminders to portal guests as the due date approaches. Requires |
{- "title": "string",
- "description": "string",
- "due_date": "2019-08-24T14:15:22Z",
- "action_type": "General",
- "assigned_guest_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "assigned_member_ids": [
- "497f6eca-6276-4993-bfeb-53cbbbba6f08"
], - "send_reminder": false
}{- "task_id": "736fde4d-9029-4915-8189-01353d6982cb"
}Updates one or more fields on a task; omitted fields keep their current values. Task IDs come from the portal's task list endpoint.
You can change status here (e.g. reopen a task by setting a non-done status), but if you just want
to finish a task, prefer POST /api/v1/tasks/{taskId}/complete — it also fires the
task.completed webhook event.
| taskId required | string <uuid> |
| title | string |
| description | string |
| status | string Use |
| due_date | string or null <date-time> |
{- "title": "string",
- "description": "string",
- "status": "string",
- "due_date": "2019-08-24T14:15:22Z"
}{- "success": true,
- "task_id": "736fde4d-9029-4915-8189-01353d6982cb"
}Marks a task as completed and updates the portal's open/closed task counters. This is the
preferred way to finish a task from an integration — for example, auto-completing a "File Request"
task after your system uploads the requested document. Fires the task.completed webhook event.
| taskId required | string <uuid> |
{- "success": true,
- "task_id": "736fde4d-9029-4915-8189-01353d6982cb"
}Invites one or more guests (clients) to a portal by email. Each guest receives an invitation email;
new users are prompted to create an account, existing users just accept the invitation. Accepts
either singular (email, name) or plural (emails, names) forms.
This is the typical second step after creating a portal: create the portal, invite the client,
upload their documents. Invitations fire the guest.invited webhook event.
Guest permissions can't be set through the API yet — new guests get your organization's default portal permissions, adjustable in the web app's Members panel. Contact support@zapaportal.com if your integration needs permission control.
| portalId required | string <uuid> |
string <email> Single guest email (alternative to | |
| name | string Single guest name (alternative to |
| emails | Array of strings <email> [ items <email > ] |
| names | Array of strings |
{- "email": "user@example.com",
- "name": "string",
- "emails": [
- "user@example.com"
], - "names": [
- "string"
]
}{- "success": true,
- "invited_count": 0,
- "message": "string"
}Returns all webhook subscriptions for your organization, including each webhook's subscribed
events, enabled status, signing secret, and delivery success/failure counters. Check the
counters to spot endpoints that are failing deliveries.
[- {
- "webhook_id": "a47606a1-5b39-4a81-9480-c2cb738ff675",
- "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",
- "events": [
- "portal.created"
], - "secret": "string",
- "enabled": true,
- "success_count": 0,
- "failure_count": 0,
- "created_at": "2019-08-24T14:15:22Z"
}
]Subscribes an HTTPS endpoint to real-time events. Deliveries are signed with HMAC-SHA256 in the
X-Webhook-Signature header — verify it with the returned secret before trusting a payload
(see the Developer Quickstart for example code). Failed deliveries
are retried 3 times with exponential backoff.
After creating, use POST /api/v1/webhooks/{webhookId}/test to send a test event and confirm your
endpoint receives and verifies it correctly.
| url required | string <uri> |
| events required | Array of strings Items Enum: "portal.created" "portal.workflow_changed" "file.uploaded" "file.signed" "task.created" "task.completed" "guest.invited" |
{- "events": [
- "portal.created"
]
}{- "webhook_id": "a47606a1-5b39-4a81-9480-c2cb738ff675",
- "org_id": "a40f5d1f-d889-42e9-94ea-b9b33585fc6b",
- "events": [
- "portal.created"
], - "secret": "string",
- "enabled": true,
- "success_count": 0,
- "failure_count": 0,
- "created_at": "2019-08-24T14:15:22Z"
}Updates the URL, subscribed events, or enabled flag; at least one field is required. Setting
enabled: false pauses deliveries without losing the configuration or signing secret — useful
during maintenance windows on your receiving endpoint.
| webhookId required | string <uuid> |
| url | string <uri> |
| events | Array of strings Items Enum: "portal.created" "portal.workflow_changed" "file.uploaded" "file.signed" "task.created" "task.completed" "guest.invited" |
| enabled | boolean |
{- "events": [
- "portal.created"
], - "enabled": true
}{- "success": true,
- "webhook_id": "a47606a1-5b39-4a81-9480-c2cb738ff675"
}Permanently removes a webhook subscription — deliveries stop immediately and the signing secret is invalidated. If you only need a temporary stop, disable it with the update endpoint instead.
| webhookId required | string <uuid> |
{- "success": true,
- "webhook_id": "a47606a1-5b39-4a81-9480-c2cb738ff675"
}Sends a signed webhook.test event to the webhook's URL and reports the delivery result — use it
to verify your endpoint is reachable and your signature verification works before relying on real
events. The response's status_code is what your endpoint returned; a success: false with an
error message usually means a network or TLS problem reaching your URL.
| webhookId required | string <uuid> |
{- "success": true,
- "status_code": 0,
- "delivery_id": "9892f438-d31c-4ff2-bc84-146525b292ff",
- "error": "string"
}