Public API Guide
This guide walks you through getting API credentials, authenticating, and making your first calls against the PartnerPortal REST API.
For the high-level overview of your integration options, see Developer Documentation. For real-time event notifications, see the Webhooks Guide.
What you can build today
Section titled “What you can build today”The REST API is lead- and partner-centric. With it you can:
- Sync leads both ways by creating, updating, fetching, and listing leads
- Discover your schema so your integration maps the right standard fields, custom fields, and picklist values
- Add partners and partner users by creating a partner organization with its owner, then adding further users to it
- Look up partners by domain or owner email, or list them all
- List the people at a partner, owners first, so you can sync them to your CRM as contacts
- Retrieve a partner’s terms acceptance history, including the exact terms text they agreed to
- Build out your content library by creating, updating and deleting announcements, resources, categories and courses, with uploaded images and attachments
- Add quiz lessons to courses, with the questions, pass mark and attempt limit set in the same call
- Receive real-time events via webhooks such as
lead/create,lead/update, andlead/delete - Automate without code via Zapier
Not available via the API today:
- partner-facing reads of the library and quiz taking (the content endpoints are for your team and API keys)
- broad bulk operations with a self-serve key
Bulk endpoints require a user-delegated token, not the self-serve keys you generate in the portal. Everything else in this guide works with a self-serve key, including creating partners and partner users one at a time. If you need server-to-server bulk imports, contact
[email protected].
A quick terms note: a lead is a deal or opportunity referred by a partner, a partner organization is a partner company in your portal, and an agent is the partner user credited for a lead.
1. Get your API credentials
Section titled “1. Get your API credentials”If your plan includes API access, you can create and manage your own API keys directly in your portal.
Generate a key
Section titled “Generate a key”- In your portal, go to Portal Setup → Other → Developer / API Keys.
- Click Create API Key, give it a recognizable name, and confirm.
- Copy the Client ID and Client Secret that are shown.
The Client Secret is shown only once. Copy it immediately and store it somewhere secure. If you lose it, revoke the key and create a new one.
You will also need:
- Audience:
https://api.partnerportal.iofor production - Company ID: the 24-character identifier for your portal
Revoke a key
Section titled “Revoke a key”From the same Developer / API Keys page, click Revoke next to any key.
Revocation takes effect promptly, although an already-issued token may continue working until it expires.
Do not see the Developer / API Keys section? API access may not be included on your current plan, or you may not have admin permissions. Contact
[email protected].
A note on key types
Section titled “A note on key types”The keys you generate are machine-to-machine credentials. They authenticate your server, not an individual user, and are the recommended way to build a backend integration.
The same credentials work for both the REST API and Webhooks.
2. Authenticate
Section titled “2. Authenticate”The API uses OAuth 2.0. Before calling any endpoint, exchange your Client ID and Client Secret for a short-lived access token.
Get an access token
Section titled “Get an access token”curl -X POST "https://login.partnerportal.io/oauth/token" \ -H "content-type: application/json" \ -d '{ "grant_type": "client_credentials", "client_id": "YOUR_CLIENT_ID", "client_secret": "YOUR_CLIENT_SECRET", "audience": "https://api.partnerportal.io" }'You will get back JSON containing an access_token.
Cache that token and reuse it until it is close to expiry instead of requesting a new one for every call.
Use the access token
Section titled “Use the access token”Include it in the Authorization header of every request:
Authorization: Bearer YOUR_ACCESS_TOKEN3. Your first API call
Section titled “3. Your first API call”The simplest way to confirm everything is wired up is to fetch your lead schema.
curl "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/schema/lead" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"A JSON response with a unifiedFields array means you are set up correctly. Each entry tells you the field’s fieldId, label, type, whether it is required, and whether it is creatable or updateable.
If you get a 401 or 403, check that:
- your access-token request returned
200 - you are using the access token, not the Client Secret, as the Bearer value
- you included the header as
Authorization: Bearer ... - you used the right
audiencefor your environment - the key has not been revoked
4. Common operations
Section titled “4. Common operations”All REST endpoints live under:
https://api.partnerportal.io/1.0/apiNon-production environments use:
https://api.partnerportal.dev/1.0/apiCreate a lead
Section titled “Create a lead”curl -X POST "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/leads" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "firstName": "Jane", "lastName": "Smith", "platform": "Enterprise", "email": "[email protected]", "partnerOrganizationId": "PARTNER_ORG_ID", "agentId": "AGENT_ID" }'A successful create returns the created lead, including its new id. Store that id, since that is how you will match incoming webhooks and fetch the lead later.
{ "id": "66a1f2c9e4b0a1234567890a", "companyId": "YOUR_COMPANY_ID", "firstName": "Jane", "lastName": "Smith", "platform": "Enterprise", "partnerOrganizationId": "PARTNER_ORG_ID", "agentId": "AGENT_ID", "status": "pending", "value": 50000, "createdAt": "2026-06-29T19:00:00.000Z", "updatedAt": "2026-06-29T19:00:00.000Z"}Required fields vary by portal, so always call GET /schema/lead first and treat each field with required: true as mandatory.
If you are using a self-serve key, partnerOrganizationId and agentId are also required on create, and the agentId must belong to the given partnerOrganizationId.
Picklist values must come from your schema rather than from an arbitrary string.
Finding your IDs
Section titled “Finding your IDs”The calls above reference a few IDs:
companyIdis the 24-character identifier for your portalpartnerOrganizationIdis returned when you create a partner or find a partner, and is on every lead you read backagentIdis returned when you add a user to a partner, and is on every lead you read back
Custom fields go under a customFields object keyed by the fieldId from your schema:
{ "firstName": "Jane", "lastName": "Smith", "platform": "Enterprise", "customFields": { "<fieldId>": "value" }}Fetch a single lead
Section titled “Fetch a single lead”curl "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/leads/LEAD_ID" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"This is the standard follow-up after receiving a webhook.
Update a lead
Section titled “Update a lead”curl -X PATCH "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/leads/LEAD_ID" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "value": 50000 }'List leads for backfill or reconciliation
Section titled “List leads for backfill or reconciliation”curl "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/leads" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"Use this to pull historical data or reconcile your copy against the portal.
Create a partner
Section titled “Create a partner”A partner is a partner organization plus its owner login. Creating one works exactly like inviting a partner from the portal: the partner is activated immediately and the owner is emailed a set-password invite.
curl -X POST "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/partners" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "companyName": "Acme, Inc.", "domain": "acme.com", "firstName": "Jane", "lastName": "Smith", "email": "[email protected]", "phone": "+15550100100" }'Every field except phone is required, and domain must be unique within your portal. The response is the created partner, including its id. That id is the partnerOrganizationId you will use everywhere else.
Add a user to a partner
Section titled “Add a user to a partner”Use this to add more users to a partner that already exists, whether you created it with the call above or it was already in your portal. The new user is activated immediately and emailed a set-password welcome, the same as adding a partner user in the portal.
curl -X POST "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/partners/PARTNER_ORG_ID/users" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "firstName": "Sam", "lastName": "Lee", "email": "[email protected]", "phone": "+15550100101" }'firstName, lastName, and email are required; phone is optional. The response is the created user, including its id, which is an agentId you can use when creating leads.
{ "id": "66a1f2c9e4b0a1234567890b", "firstName": "Sam", "lastName": "Lee", "phone": "+15550100101", "type": "agent", "status": "active", "partnerOrganizationId": "PARTNER_ORG_ID", "createdAt": "2026-09-07T19:00:00.000Z", "updatedAt": "2026-09-07T19:00:00.000Z"}A few rules apply:
- the partner must belong to your portal (otherwise you get a
404) and be active - the email must not already be in use by anyone in your portal
- your plan’s per-partner user limit still applies
Every call sends a real email. Both Create a partner and Add a user to a partner email the person a set-password link, exactly as the portal does. Use test data while you are building, and tell partners to expect the email before you run a migration.
Find a partner
Section titled “Find a partner”To avoid creating a duplicate, look a partner up by domain or by its owner’s email before creating it. Either filter turns the partner list into a single-match lookup.
curl "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/partners?domain=acme.com" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"You get back a list with the one matching partner, or an empty list if nothing matches. If more than one partner matches, the call returns an error instead of guessing, so use a value that identifies exactly one partner. Without a filter, the same call lists every partner in your portal.
A partner record also carries its terms status: termsAcceptedAt is when the partner last accepted your program terms, and acceptedTermsVersion (or acceptedGroupTermsVersion, for a partner group’s own terms) is the version they accepted. Acceptance covers the whole partner, so these live on the partner rather than on each user.
List a partner’s users
Section titled “List a partner’s users”A partner can have more than one owner, plus users added under it. This call lists them all, which is how you get the people behind a partner — for example, to create or update them as contacts in your CRM.
curl "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/partners/PARTNER_ORG_ID/users" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"Each user has the same fields as the Add a user response above. type is partner for an owner and agent for a user added under the partner. The list is ordered active users first, then owners, then oldest first. So the first item is the partner’s longest-standing active owner, or, if no owner is active, its longest-standing active user.
Three optional filters narrow the list:
type=partnerortype=agentreturns only owners or only added usersstatus=activereturns only active users (other values:pending,declined,suspended)[email protected]returns the user with that email
If the partner does not belong to your portal, you get a 404. If you call this with a partner’s own login rather than a key, it only works for that login’s own partner, and only if that login can see the partner’s users in the portal.
Get a partner’s terms acceptance history
Section titled “Get a partner’s terms acceptance history”Every time a partner accepts your program terms, PartnerPortal keeps a permanent record of it: who accepted, when, which version, and an exact copy of the terms text they agreed to. Use this to retrieve that history if a partner’s agreement is ever questioned.
curl "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/partners/PARTNER_ORG_ID/termsAcceptances" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"You get back the acceptances newest first. Each one includes acceptedAt, the accepting person (acceptedByFirstName, acceptedByLastName, acceptedByEmail), termsVersion, and scope — company for your program terms or group for a partner group’s own terms, with its groupName. The list leaves out the terms text to stay light. To get the exact text for one acceptance, fetch it by id:
curl "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/partners/PARTNER_ORG_ID/termsAcceptances/ACCEPTANCE_ID" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"The response adds termsSnapshot, the terms HTML exactly as it was when the partner agreed.
A few things to know:
- Acceptance belongs to the partner. One owner accepts on behalf of the whole partner, so only that person is listed. Other owners and users are covered without an acceptance of their own.
- The version number changes on every edit to your terms, including small fixes, so it identifies an edit rather than a named document. The saved terms text is the reliable record.
- History starts when record-keeping began. Acceptances from before PartnerPortal started keeping these records only appear as the partner’s
termsAcceptedAtdate. - The history is for your team only. A partner’s own login cannot read it.
You can see the same history in the portal on the partner’s page, under Terms History.
Upload a file
Section titled “Upload a file”Announcements, resources and courses can carry files: a featured image, images inside the body, and attachments partners can download. Files are uploaded first and referenced afterwards by id or address. An upload is three steps, because the file goes straight to storage rather than through the API:
1. Ask for an upload slot. Say what the file is for: image for a picture that will be shown (public, served from our CDN, images only, 20 MB), or attachment for a download such as a PDF or video (private, served to partners only through the resource it is attached to, 50 MB). The file name’s extension decides the stored type.
curl -X POST "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/assets/slots" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "fileName": "partner-guide.pdf", "purpose": "attachment" }'The response holds a slotId, an upload.url, an upload.fields object and an expiresAt time about fifteen minutes out. Treat the slotId as an opaque string; expiresAt is when the upload form stops working. Nothing is stored on our side until you complete the slot.
2. Send the file to the upload address. Post a multipart form to upload.url with every entry of upload.fields as a form field, then the file itself as file, last. With curl, one -F per field:
curl -X POST "UPLOAD_URL" \ -F "key=VALUE_FROM_FIELDS" \ -F "Content-Type=VALUE_FROM_FIELDS" \ -F "Policy=VALUE_FROM_FIELDS" \ -F "X-Amz-Algorithm=VALUE_FROM_FIELDS" \ -F "X-Amz-Credential=VALUE_FROM_FIELDS" \ -F "X-Amz-Date=VALUE_FROM_FIELDS" \ -F "X-Amz-Signature=VALUE_FROM_FIELDS" \ -F "file=@./partner-guide.pdf"The field names vary, so copy whatever upload.fields contains rather than this list. A 204 means the file landed. The form only accepts the exact name, type and size range the slot was created for.
3. Complete the slot.
curl -X POST "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/assets/slots/SLOT_ID/complete" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"You get back the stored file: its id, fileName, size, contentType, and for images a url. The id is what you pass as thumbnailAssetId or inside attachmentIds; the url is what you put in body HTML as an image source. Completing the same slot twice returns the same file. A 400 saying no file has been uploaded means step 2 has not happened or the form expired: ask for a new slot and upload again. A 404 means the slotId is not one of yours. A file that is uploaded but never referenced stays in storage, so complete only what you intend to use.
Each upload belongs to one item. A featured image or attachment id can be used on one resource or announcement; using it on a second one is refused, with the owning item named in the error. Upload the file again to use it elsewhere. When an item replaces or removes its featured image, or drops an attachment from attachmentIds, that file is deleted. An image used only inside body HTML is referenced by its url and is not tied to an item, so do not use the same upload as both a body image and a featured image or attachment. An attachment id cannot be used as a featured image or the reverse; the error says which kind it is.
Write body HTML
Section titled “Write body HTML”Announcement and resource bodies are HTML, the same HTML the portal’s editor produces. The server cleans every body on save and keeps paragraphs, headings, lists, tables, links, images with absolute https sources, basic inline styles, and video embeds (iframes) from YouTube, Vimeo, Loom, Wistia, Canva, iorad and Navattic. Scripts, event handlers and anything else are removed silently, which is why every create and update returns the stored body: compare it with what you sent.
Two editor features are plain links with a marker:
- a button is
<a class="btn btn-primary" href="https://...">Book a call</a>(orbtn-outline-primaryfor the outline style) - an in-app link that opens inside the portal is
<a href="/leads/register" data-ppio-internal-link="true">Register a lead</a>
Create a category or course
Section titled “Create a category or course”A course is a category of type course; its resources are its lessons. Partners see a category only if it is shared with one of their partner groups (groupIds) or with their partner directly (partnerOrganizationIds). You can create it first and assign the audience later, but until you do, no partner can see it and the response says so in warnings.
curl -X POST "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/categories" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Partner Onboarding", "type": "course", "welcomeMessage": "<p>Welcome! Work through these lessons in order.</p>", "groupIds": ["PARTNER_GROUP_ID"] }'GET .../categories lists categories and courses (?type=course narrows it), GET .../categories/{id} returns one with its lessons in order for a course, and PATCH .../categories/{id} changes the fields you send. groupIds and partnerOrganizationIds replace the current audience when sent. The type cannot be changed after creation.
Create a resource or lesson
Section titled “Create a resource or lesson”A resource is a post: a name, an HTML body, an optional excerpt and featured image, one or more categories, and attachments. Putting a course id in categoryIds makes it a lesson in that course.
curl -X POST "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/resources" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Lesson 1: Your first referral", "excerpt": "How to register a lead and what happens next.", "body": "<p>Start here.</p><img src=\"https://cdn.partnerportal.io/public/IMAGE_FROM_UPLOAD.png\"><p>...</p>", "categoryIds": ["COURSE_ID"], "thumbnailAssetId": "IMAGE_ASSET_ID", "attachmentIds": ["ATTACHMENT_ASSET_ID"] }'GET .../resources lists resources (?categoryId= narrows to one category or course, ?search= matches names), GET .../resources/{id} returns one, and PATCH .../resources/{id} changes the fields you send. categoryIds and attachmentIds are complete lists that replace the current ones; an empty attachmentIds removes every attachment, and categoryIds may not be empty. Send "thumbnailAssetId": null to remove the featured image. A removed featured image or attachment is deleted, so upload a fresh file if you want it back. The legacy file, video-embed and link resource types cannot be created here; they are read back as they are.
To put lessons in order, send every lesson id in the wanted order:
curl -X PUT "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/categories/COURSE_ID/lessons/order" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "resourceIds": ["LESSON_1_ID", "LESSON_2_ID", "LESSON_3_ID"] }'The list must name every lesson in the course, each once.
Create a quiz lesson
Section titled “Create a quiz lesson”A quiz lesson is a resource of type quiz in a course. Partners complete it by passing it, not by marking it done, and the course’s completion and any certification gated on it follow from that. Send the questions with the lesson; only question text and choice text are required.
curl -X POST "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/resources" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Onboarding basics check", "type": "quiz", "categoryIds": ["COURSE_ID"], "quiz": { "passingPercent": 80, "questions": [ { "text": "Which plan includes SSO?", "choices": [ { "text": "Starter" }, { "text": "Essentials", "isCorrect": true }, { "text": "Free" } ] }, { "text": "Which of these are payout methods?", "choices": [ { "text": "PayPal", "isCorrect": true }, { "text": "Stripe", "isCorrect": true }, { "text": "Cash" } ] } ] } }'What is filled in for you: question and choice ids, their order (the order you sent), a question’s type (single when one choice is correct, multiple when more than one is, in which case the partner must pick all of them), a pass mark of 70 percent and unlimited attempts. Set passingPercent (1 to 100) and maxAttempts (a whole number, or null for unlimited) to change them. A definition that is not valid is refused with the first problem as the message and the full list in the details, and nothing is created. Every category id must be a course.
GET .../resources/{id}/quiz returns the stored definition, answer key included, and PUT .../resources/{id}/quiz replaces it:
curl -X PUT "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/resources/QUIZ_LESSON_ID/quiz" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "passingPercent": 90 }'questions replaces every question when sent (send each question’s id back to keep it); leave it out to change only the pass mark or attempt limit. The body takes only questions, passingPercent and maxAttempts: when you edit a GET result and send it back, drop its resourceId and updatedAt (the id and order fields inside questions and choices may stay). Partners’ past attempts keep what they were shown, so editing never changes a recorded score. A lesson’s type cannot be changed afterwards: to turn a post into a quiz, delete it and create a quiz lesson. The quiz routes are for your team and API keys; a partner takes the quiz in the portal and never sees which choices are correct.
Create an announcement
Section titled “Create an announcement”curl -X POST "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/announcements" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "title": "New partner guide available", "body": "<p>The 2027 guide is live. <a class=\"btn btn-primary\" href=\"/resources\" data-ppio-internal-link=\"true\">Open resources</a></p>", "excerpt": "The 2027 partner guide is now in your resource library.", "thumbnailAssetId": "IMAGE_ASSET_ID" }'A new announcement is a draft unless you say otherwise. Publishing makes it live for partners and cannot be undone, so it has to be asked for explicitly with "status": "published", on create or on a later PATCH. Emails are opt-in, as in the portal’s own editor: set "sendNotifications": true to email the audience when it publishes (the partners you chose, plus your portal team when visibleToTeam is true); left out, it publishes quietly. The response tells you what happened: published is true when this call made it live, and notificationsSent is true when emails went out. The audience has two parts. visibility says which partners see it: all (the default when omitted), select-partner-groups with selectedPartnerGroupIds, select-partners with selectedPartnerOrganizationIds, or none. visibleToTeam says whether your own admins and staff are emailed and notified about it; it is true when omitted. An announcement cannot be published with an empty audience: a group or partner restriction must name at least one, and none must include the team.
GET .../announcements lists announcements newest first, drafts included (?status=draft or ?status=published narrows it), GET .../announcements/{id} returns one, and PATCH .../announcements/{id} changes the fields you send.
Delete content
Section titled “Delete content”curl -X DELETE "https://api.partnerportal.io/1.0/api/company/YOUR_COMPANY_ID/resources/RESOURCE_ID" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN"DELETE .../resources/{id}, DELETE .../announcements/{id} and DELETE .../categories/{id} each answer { "id": "...", "deleted": true }. Deletes are final. Nothing is kept for restoring, so read the item first if you may need it again.
- Deleting a resource also deletes its featured image and attachments and, for a quiz lesson, its questions and every partner’s attempts. It leaves every category and course it was in.
- Deleting an announcement also deletes its featured image and the in-app notifications about it. Emails already sent are not recalled.
- Deleting a category or course keeps the resources in it. It is refused while any of them is in no other category, so move those resources first (
PATCHwith newcategoryIds) or delete them.
The content endpoints are for your team and API keys. A partner’s own login cannot call them.
5. Common integration patterns
Section titled “5. Common integration patterns”Receive new leads in your system
Section titled “Receive new leads in your system”- Subscribe to the
lead/createwebhook topic. - When a webhook arrives, verify the signature and read the lead
id. - Call
GET /company/YOUR_COMPANY_ID/leads/{id}to fetch the full record. - Save it in your system, using
topic + id + timestampas a deduplication key.
Send leads into PartnerPortal
Section titled “Send leads into PartnerPortal”- Pull your schema once at setup with
GET /schema/lead. - For each lead,
POST /company/YOUR_COMPANY_ID/leadswith the mapped body.
Periodic backfill or reconciliation
Section titled “Periodic backfill or reconciliation”Webhooks are forward-looking. To pull historical data or catch anything missed during an outage, periodically list leads and diff against your local copy.
Migrate partners from another system
Section titled “Migrate partners from another system”Moving your partner list out of a spreadsheet, a CRM, or another partner platform is a loop over two calls with a self-serve key:
- For each partner,
POST /company/YOUR_COMPANY_ID/partnerswith the company and owner details, and store the returnedid. - For each additional user on that partner,
POST /company/YOUR_COMPANY_ID/partners/{id}/users.
Every call emails one person a set-password link, so run the migration in sequence rather than all at once, and let your partners know the email is coming.
Sync partner owners to your CRM
Section titled “Sync partner owners to your CRM”To keep the people behind your partners in your CRM as contacts:
- Periodically list your partners, or use the Zapier Partner Updated trigger to react to changes as they happen.
- For each new or changed partner, call
GET /company/YOUR_COMPANY_ID/partners/{id}/users?status=activeand upsert each user, or just the first one (the longest-standing active owner), as a contact. - Copy
termsAcceptedAtandacceptedTermsVersionfrom the partner record onto the contact if you track terms in your CRM.
Build out your content library from files and scripts
Section titled “Build out your content library from files and scripts”Moving a library of guides, decks and videos into your portal, or generating courses from content you already have, is a loop over the upload and content calls:
- Create the course or category once with
POST /company/YOUR_COMPANY_ID/categories, and keep itsid. - For each file, run the three upload steps and keep the returned
id(andurlfor images). - For each resource,
POST /company/YOUR_COMPANY_ID/resourceswith the body HTML, the course id incategoryIds, the image id asthumbnailAssetIdand the file ids inattachmentIds. - Finish with
PUT .../categories/{id}/lessons/orderto fix the lesson order, thenPATCHthe course with itsgroupIdsto make it visible.
Keep announcements for last and leave them as drafts until someone has read them in the portal; publishing with sendNotifications on emails your partners.
If you work with an AI assistant, the same operations are available as tools through the PartnerPortal MCP server, including an upload from a public URL.
6. Good practices
Section titled “6. Good practices”- cache your access token and refresh it only as it nears expiry
- be reasonable with request volume and back off if you receive a
429 - verify webhook signatures instead of trusting source IPs
- rotate keys you no longer use and revoke immediately if a secret may have leaked
Where to go next
Section titled “Where to go next”- Webhooks Guide
- Developer Documentation
- Zapier guide
- developer.partnerportal.io/docs for the full endpoint reference
Need help?
Section titled “Need help?”If you run into auth errors, unexpected responses, or design questions, contact [email protected].