PageSourceSearch

https://plaid.com/_next/static/chunks/pages/core-exchange/docs/con…/permissions-manager-347d5cc5ed740429.js

js plaid.com collected 2026-09-24 07:18:40 UTC 36,396 bytes, 2 lines download raw bytes

1try{let e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},t=(new e.Error).stack;t&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[t]="6353d68e-c738-4e27-aa06-0a84a22084aa",e._sentryDebugIdIdentifier="sentry-dbid-6353d68e-c738-4e27-aa06-0a84a22084aa")}catch(e){}(self.webpackChunk_N_E=self.webpackChunk_N_E||[]).push([[9864],{19583:function(e,t,a){"use strict";a.r(t),a.d(t,{default:function(){return y},metadata:function(){return d},tableOfContents:function(){return f}});var n=a(36864),i=a(4730);a(67294);var o=a(3905),r=a(47608),s=a(90182),l=a(43402),p=a.n(l),c=["components"],d={toc:!0,subnav:!0,title:"Permissions Manager API",layout:"guide",description:"Read connected applications, consent activity, and revoke access",parentTocLevel:2,childTocLevel:3,secondLevelToc:!0,alwaysExpand:!0},m=function(e){return function(t){return console.warn("Component "+e+" was not imported, exported, or provided by MDXProvider as global scope"),(0,o.kt)("div",t)}},u=m("Header"),k=m("Callout"),h=m("Schema"),N=m("Image"),g={metadata:d};function y(e){var t=e.components,a=(0,i.Z)(e,c);return(0,o.kt)("wrapper",(0,n.Z)({},g,a,{components:t,mdxType:"MDXLayout"}),(0,o.kt)("div",{className:p().page},(0,o.kt)(u,{title:"Permissions Manager API",subtitle:d.description,mdxType:"Header"}),(0,o.kt)("h2",{id:"overview"},"Overview"),(0,o.kt)(k,{warning:!0,mdxType:"Callout"},(0,o.kt)("p",null,"This page is provided as a reference for data partners already using the Plaid Permissions Manager API. New integrations should instead use the FDX-aligned ",(0,o.kt)("a",{parentName:"p",href:"/core-exchange/docs/consent-management/api"},"Consent API"),", which models consent as an FDX consent grant, allowing you to build a single consent management integration serving multiple data access platforms. "),(0,o.kt)("p",null,"If you are an existing Permissions Manager user interested in migrating, see ",(0,o.kt)("a",{parentName:"p",href:"/core-exchange/docs/consent-management/api#migrating-from-permissions-manager-to-the-consent-api"},"Migrating from Permissions Manager to the Consent API"),". ")),(0,o.kt)("p",null,"The Permissions Manager API reports which Plaid-powered applications one of your customers has connected, which accounts and data types each application can read, and when each application last read them. It also lets you revoke an application's access."),(0,o.kt)("p",null,"It is the API behind a consumer-facing consent portal on your domain: the customer sees their connections and disconnects the ones they no longer want, and Plaid enforces that revocation across the ecosystem. Single-institution accounts can read the same data with no code in the ",(0,o.kt)("a",{parentName:"p",href:"/core-exchange/docs/consent-management"},"Data Partner Dashboard"),", whose Permissions Manager tabs call these same endpoints; this page covers the API, which is available to every integration model, including platforms."),(0,o.kt)("p",null,"For conceptual background on authorization records and the webhooks that accompany these endpoints, see ",(0,o.kt)("a",{parentName:"p",href:"/core-exchange/docs/consent-management"},"Consent management"),"."),(0,o.kt)("h2",{id:"authentication"},"Authentication"),(0,o.kt)("p",null,"Find your ",(0,o.kt)("inlineCode",{parentName:"p"},"client_id")," and ",(0,o.kt)("inlineCode",{parentName:"p"},"secret")," on the ",(0,o.kt)("a",{parentName:"p",href:"https://dashboard.plaid.com/developers/keys"},"Developer > Keys")," tab of the Data Partner Dashboard. Include both as the ",(0,o.kt)("inlineCode",{parentName:"p"},"PLAID-CLIENT-ID")," and ",(0,o.kt)("inlineCode",{parentName:"p"},"PLAID-SECRET")," headers, or as ",(0,o.kt)("inlineCode",{parentName:"p"},"client_id")," and ",(0,o.kt)("inlineCode",{parentName:"p"},"secret")," in the request body. All requests must be made over HTTPS."),(0,o.kt)("h2",{id:"customer-identifiers-and-access-tokens"},"Customer identifiers and access tokens"),(0,o.kt)("p",null,"Permissions Manager identifies a customer by an ",(0,o.kt)("inlineCode",{parentName:"p"},"access_token"),", not by a customer identifier, so every integration starts with ",(0,o.kt)("a",{parentName:"p",href:"#exchange-a-customer-identifier-for-an-access-token"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/import")),"."),(0,o.kt)("p",null,"Use the same persistent, unique identifier you use as the ",(0,o.kt)("inlineCode",{parentName:"p"},"sub")," claim in your OIDC ID token — see ",(0,o.kt)("a",{parentName:"p",href:"/core-exchange/docs/authentication/oauth-server-setup/#unique-user-identifier-consistency-key"},"Unique user identifier (consistency key)"),". Reusing an identifier across two customers merges their connections; changing it for an existing customer strands the connections recorded under the old one."),(0,o.kt)("p",null,"Because ",(0,o.kt)("a",{parentName:"p",href:"#exchange-a-customer-identifier-for-an-access-token"},(0,o.kt)("inlineCode",{parentName:"a"}
1,"/item/import"))," is idempotent, you can call it lazily the first time a customer opens your portal, batch it ahead of time, or call it again whenever you need the token."),(0,o.kt)("h3",{id:"exchange-a-customer-identifier-for-an-access-token"},"Exchange a customer identifier for an access token"),(0,o.kt)("h4",{id:"post-itemimport"},(0,o.kt)("inlineCode",{parentName:"h4"},"POST /item/import")),(0,o.kt)(h,{method:"POST",route:"/item/import",type:"request",mdxType:"Schema"},(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},'curl -X POST \'https://production.plaid.com/item/import\' \\\n  -H \'Content-Type: application/json\' \\\n  -d \'{\n    "client_id": "<client_id>",\n    "secret": "<secret>",\n    "user_auth": {\n      "user_id": "<user_id>"\n    }\n  }\'\n'))),(0,o.kt)(h,{method:"POST",route:"/item/import",type:"response",mdxType:"Schema"}),(0,o.kt)("h2",{id:"endpoints"},"Endpoints"),(0,o.kt)("table",null,(0,o.kt)("thead",{parentName:"table"},(0,o.kt)("tr",{parentName:"thead"},(0,o.kt)("th",{parentName:"tr",align:null},"Endpoint"),(0,o.kt)("th",{parentName:"tr",align:null},"Functionality"))),(0,o.kt)("tbody",{parentName:"table"},(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("a",{parentName:"td",href:"#exchange-a-customer-identifier-for-an-access-token"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/import"))),(0,o.kt)("td",{parentName:"tr",align:null},"Exchange your customer identifier for an access token")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("a",{parentName:"td",href:"#list-a-customers-connected-applications"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/application/list"))),(0,o.kt)("td",{parentName:"tr",align:null},"List connected and disconnected applications, with their scopes")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("a",{parentName:"td",href:"#read-consent-activity-and-last-access-times"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/activity/list"))),(0,o.kt)("td",{parentName:"tr",align:null},"Read consent activity history and per-scope last-access times")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("a",{parentName:"td",href:"#revoke-an-applications-access"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/application/unlink"))),(0,o.kt)("td",{parentName:"tr",align:null},"Revoke an application's access to a customer's data")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("a",{parentName:"td",href:"#testing-in-sandbox"},(0,o.kt)("inlineCode",{parentName:"a"},"/sandbox/item/application/seed"))),(0,o.kt)("td",{parentName:"tr",align:null},"Sandbox only: connect a demo application to a customer")))),(0,o.kt)("h3",{id:"list-a-customers-connected-applications"},"List a customer's connected applications"),(0,o.kt)("h4",{id:"post-itemapplicationlist"},(0,o.kt)("inlineCode",{parentName:"h4"},"POST /item/application/list")),(0,o.kt)("p",null,"Call this each time a customer opens your portal rather than serving a cached copy, so that revocations made elsewhere in the ecosystem are reflected immediately."),(0,o.kt)("p",null,"The response separates ",(0,o.kt)("inlineCode",{parentName:"p"},"applications"),", which the customer has authorized, from ",(0,o.kt)("inlineCode",{parentName:"p"},"disconnected_applications"),", which they authorized and later revoked. Render disconnected applications as history: they carry no ",(0,o.kt)("inlineCode",{parentName:"p"},"scopes")," or ",(0,o.kt)("inlineCode",{parentName:"p"},"created_at"),", because there is no longer any access to describe."),(0,o.kt)("p",null,"Parse ",(0,o.kt)("inlineCode",{parentName:"p"},"created_at")," permissively. Depending on when your integration was set up, it arrives either as an ISO 8601 datetime (",(0,o.kt)("inlineCode",{parentName:"p"},"2020-01-01T00:00:00Z"),") or as a date alone (",(0,o.kt)("inlineCode",{parentName:"p"},"2020-01-01"),")."),(0,o.kt)(h,{method:"POST",route:"/item/application/list",type:"request",mdxType:"Schema"},(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},'curl -X POST \'https://production.plaid.com/item/application/list\' \\\n  -H \'Content-Type: application/json\' \\\n  -d \'{\n    "client_id": "<client_id>",\n    "secret": "<secret>",\n    "access_token": "<access_token>"\n  }\'\n'))),(0,o.kt)(h,{method:"POST",route:"/item/application/list",type:"response",mdxType:"Schema"}),(0,o.kt)("h4",{id:"reading-the-scopes-object"},"Reading the scopes object"),(0,o.kt)("p",null,(0,o.kt)("inlineCode",{parentName:"p"},"scopes.product_access")," maps a data type to whether the customer authorized it. The keys that can appear are:"),(0,o.kt)("table",null,(0,o.kt)("thead",{parentName:"table"},(0,o.kt)("tr",{parentName:"thead"},(0,o.kt)("th",{parentName:"tr",align:null},"Key"),(0,o.kt)("th",{parentName:"tr",align:null},"Includes"))),(0,o.kt)("tbody",{parentName:"table"},(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"account_balance_info")),(0,o.kt)("td",{parentName:"tr",align:null},"Account name, type, description, balances, and masked account number")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"contact_info")),(0,o.kt)("td",{parentName:"tr",align:null},"Account owner name, email, phone, and address")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"account_routing_number")),(0,o.kt)("td",{parentName:"tr",align:null},"Account and routing numbers")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"transactions")),(0,o.kt)("td",{parentName:"tr",align:null},"Transaction amounts, dates, descriptions, and categories, and derived insights")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"credit_loan_info")),(0,o.kt)("td",{parentName:"tr",align:null},"Balances, payment dates and amounts due, credit limits, rates, and loan terms")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"investments")),(0,o.kt)("td",{parentName:"tr",align:null},"Securities details, quantities, prices, and investment transactions")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"bank_statements")),(0,o.kt)("td",{parentName:"tr",align:null},"PDF statements")))),(0,o.kt)("p",null,'Only the data types an application requested appear, so treat a missing key as "not authorized" rather than as an error. Applications built on Plaid products outside this set can introduce further keys, so parse ',(0,o.kt)("inlineCode",{parentName:"p"},"product_access")," as a map rather than a fixed struct."),(0,o.kt)(k,{mdxType:"Callout"},(0,o.kt)("p",null,"If you integrated before June 2024, your ",(0,o.kt)("inlineCode",{parentName:"p"},"product_access")," keys may use different field names than the ones in the table above. Contact your Plaid solutions engineering team if you'd like to migrate to the new field names.")),(0,o.kt)("p",null,(0,o.kt)("inlineCode",{parentName:"p"},"scopes.accounts")," lists the customer's accounts by ",(0,o.kt)("inlineCode",{parentName:"p"},"unique_id")," — the same account identifier your FDX API returns — each with whether the application may read it. ",(0,o.kt)("inlineCode",{parentName:"p"},"new_accounts")," reports whether accounts the customer opens later are included automatically. An empty ",(0,o.kt)("inlineCode",{parentName:"p"},"accounts")," array on a long-standing connection means the customer authorized every available account; those connections predate per-account selection."),(0,o.kt)("p",null,"Where you scope data types per account rather than per connection, each account also carries ",(0,o.kt)("inlineCode",{parentName:"p"},"account_product_access"),", keyed by the per-account scopes configured for your integration."),(0,o.kt)("h3",{id:"read-consent-activity-and-last-access-times"},"Read consent activity and last access times"),(0,o.kt)("h4",{id:"post-itemactivitylist"},(0,o.kt)("inlineCode",{parentName:"h4"},"POST /item/activity/list")),(0,o.kt)("p",null,(0,o.kt)("inlineCode",{parentName:"p"},"/item/activity/list")," is the audit view of your portal: what happened to a customer's connections, and when each application last read their data. Results are paginated: pass ",(0,o.kt)("inlineCode",{parentName:"p"},"count")," to size a page and the ",(0,o.kt)("inlineCode",{parentName:"p"},"cursor")," from the previous response to fetch the next one. An omitted ",(0,o.kt)("inlineCode",{parentName:"p"},"cursor")," in the response means you have reached the end."),(0,o.kt)(h,{method:"POST",route:"/item/activity/list",type:"request",mdxType:"Schema"},(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},'curl -X POST \'https://production.plaid.com/item/activity/list\' \\\n  -H \'Content-Type: application/json\' \\\n  -d \'{\n    "client_id": "<client_id>",\n    "secret": "<secret>",\n    "access_token": "<access_token>"\n  }\'\n'))),(0,o.kt)("p",null,"Each entry in ",(0,o.kt)("inlineCode",{parentName:"p"},"activities")," records one event. ",(0,o.kt)("inlineCode",{parentName:"p"},"activity")," is the event type, ",(0,o.kt)("inlineCode",{parentName:"p"},"initiated_date")," is when it happened, ",(0,o.kt)("inlineCode",{parentName:"p"},"state")," is its outcome, ",(0,o.kt)("inlineCode",{parentName:"p"},"initiator")," identifies who caused it, and ",(0,o.kt)("inlineCode",{parentName:"p"},"target_application_id")," names the application it acted on:"),(0,o.kt)("table",null,(0,o.kt)("thead",{parentName:"table"},(0,o.kt)("tr",{parentName:"thead"},(0,o.kt)("th",{parentName:"tr",align:null},"activity"),(0,o.kt)("th",{parentName:"tr",align:null},"Recorded when"))),(0,o.kt)("tbody",{parentName:"table"},(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"ITEM_CREATE")),(0,o.kt)("td",{parentName:"tr",align:null},"The customer connected an application")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"ITEM_IMPORT")),(0,o.kt)("td",{parentName:"tr",align:null},"You called ",(0,o.kt)("a",{parentName:"td",href:"#exchange-a-customer-identifier-for-an-access-token"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/import"))," for the customer")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"ITEM_UPDATE")),(0,o.kt)("td",{parentName:"tr",align:null},"An existing connection was reauthorized or its scopes changed")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"ITEM_UNLINK")),(0,o.kt)("td",{parentName:"tr",align:null},"An application's access ended, including through ",(0,o.kt)("a",{parentName:"td",href:"#revoke-an-applications-access"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/application/unlink")))),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"PORTAL_UNLINK")),(0,o.kt)("td",{parentName:"tr",align:null},"The customer disconnected an application in ",(0,o.kt)("a",{parentName:"td",href:"https://my.plaid.com"},"Plaid Portal"))),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"PORTAL_ITEMS_DELETE")),(0,o.kt)("td",{parentName:"tr",align:null},"The customer deleted their data in Plaid Portal")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"ITEM_REMOVE")),(0,o.kt)("td",{parentName:"tr",align:null},"The connected application removed the connection on its own side")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"SCOPES_UPDATE")),(0,o.kt)("td",{parentName:"tr",align:null},"The scopes on a connection were updated")))),(0,o.kt)("p",null,(0,o.kt)("inlineCode",{parentName:"p"},"last_data_access_times")," holds one object per application, keyed by ",(0,o.kt)("inlineCode",{parentName:"p"},"application_id"),", with a timestamp for every ",(0,o.kt)("inlineCode",{parentName:"p"},"product_access")," scope except ",(0,o.kt)("inlineCode",{parentName:"p"},"bank_statements"),". A ",(0,o.kt)("inlineCode",{parentName:"p"},"null")," timestamp means the application has never read that data type. This is the field to show a customer who wants to know whether an app they authorized months ago is still reading their accounts."),(0,o.kt)(k,{mdxType:"Callout"},(0,o.kt)("p",null,"If you integrated before June 2024, your ",(0,o.kt)("inlineCode",{parentName:"p"},"last_data_access_times")," object may use different field names than the ones above, and may also include ",(0,o.kt)("inlineCode",{parentName:"p"},"payroll_info")," and ",(0,o.kt)("inlineCode",{parentName:"p"},"transaction_risk_info"),", which are deprecated. Contact your Plaid solutions engineering team if you'd like to migrate to the new field names.")),(0,o.kt)("h3",{id:"revoke-an-applications-access"},"Revoke an application's access"),(0,o.kt)("h4",{id:"post-itemapplicationunlink"}
1,(0,o.kt)("inlineCode",{parentName:"h4"},"POST /item/application/unlink")),(0,o.kt)("p",null,"Call this endpoint as soon as a customer disconnects an application in your portal, one call per application, and never in a batch — Plaid revokes the application's access on receipt, and the delay before that call is time the application can still read data."),(0,o.kt)("p",null,"If you offer users the ability to revoke access, you must keep the ecosystem in sync:"),(0,o.kt)("ol",null,(0,o.kt)("li",{parentName:"ol"},(0,o.kt)("strong",{parentName:"li"},"User revokes access on your domain:")," The customer visits your consent portal and disconnects an application"),(0,o.kt)("li",{parentName:"ol"},(0,o.kt)("strong",{parentName:"li"},"Call ",(0,o.kt)("a",{parentName:"strong",href:"#revoke-an-applications-access"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/application/unlink")),":")," Your system calls Plaid's revocation endpoint"),(0,o.kt)("li",{parentName:"ol"},(0,o.kt)("strong",{parentName:"li"},"Plaid notifies the application:")," Plaid sends the application a ",(0,o.kt)("inlineCode",{parentName:"li"},"USER_PERMISSION_REVOKED")," webhook"),(0,o.kt)("li",{parentName:"ol"},(0,o.kt)("strong",{parentName:"li"},"Plaid severs the connection:")," Plaid blocks the application from accessing data"),(0,o.kt)("li",{parentName:"ol"},(0,o.kt)("strong",{parentName:"li"},"Application stops requesting data:")," The application knows the customer revoked access and stops making requests")),(0,o.kt)("p",null,"Without step 2, the application keeps its access and your portal shows a disconnection that never took effect."),(0,o.kt)("p",null,"A customer who wants the application again has to go through your OAuth flow from the start; there is no way to restore a revoked connection."),(0,o.kt)(h,{method:"POST",route:"/item/application/unlink",type:"request",mdxType:"Schema"},(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},'curl -X POST \'https://production.plaid.com/item/application/unlink\' \\\n  -H \'Content-Type: application/json\' \\\n  -d \'{\n    "client_id": "<client_id>",\n    "secret": "<secret>",\n    "access_token": "<access_token>",\n    "application_id": "<application_id>"\n  }\'\n'))),(0,o.kt)(h,{method:"POST",route:"/item/application/unlink",type:"response",mdxType:"Schema"}),(0,o.kt)("h2",{id:"webhooks"},"Webhooks"),(0,o.kt)("p",null,"Plaid can optionally send webhooks to you when authorization events occur. They are independent of which API you read with. Single-institution accounts can configure them in the Data Partner Dashboard via the ",(0,o.kt)("a",{parentName:"p",href:"https://dashboard.plaid.com/developers/webhooks"},"Developers > Webhooks")," page. Platform accounts should reach out to your Plaid contact to have these enabled."),(0,o.kt)(N,{src:"/assets/img/core-exchange/permissions-manager/webhooks.png",alt:"Webhook configuration interface in the Data Partner Dashboard",caption:"Configure webhooks for real-time connection and disconnection alerts",expandable:!0,mdxType:"Image"}),(0,o.kt)("p",null,"Plaid sends two webhooks: ",(0,o.kt)("a",{parentName:"p",href:"#authorization_granted"},(0,o.kt)("inlineCode",{parentName:"a"},"AUTHORIZATION_GRANTED"))," and ",(0,o.kt)("a",{parentName:"p",href:"#consent_revoked"},(0,o.kt)("inlineCode",{parentName:"a"},"CONSENT_REVOKED")),". For the payload shape and full examples, see ",(0,o.kt)("a",{parentName:"p",href:"/core-exchange/docs/consent-management/api#webhooks"},"Webhooks")," on the Consent API reference."),(0,o.kt)("h3",{id:"authorization_granted"},(0,o.kt)("inlineCode",{parentName:"h3"},"AUTHORIZATION_GRANTED")),(0,o.kt)("p",null,"Notifies you every time a customer connects a new application via Plaid. While your system will already know about new OAuth authorizations, this webhook also alerts you to connections created via the returning user experience, where the customer never goes through your OAuth flow and this webhook is your only real-time signal that a new connection was created."),(0,o.kt)("p",null,(0,o.kt)("strong",{parentName:"p"},"When it fires:")," After a customer successfully authorizes a new application."),(0,o.kt)("p",null,(0,o.kt)("strong",{parentName:"p"},"Payload includes:")," The customer (",(0,o.kt)("inlineCode",{parentName:"p"},"user_identifier"),") and application details (",(0,o.kt)("inlineCode",{parentName:"p"},"application_id"),", ",(0,o.kt)("inlineCode",{parentName:"p"},"application_name"),")."),(0,o.kt)("p",null,(0,o.kt)("strong",{parentName:"p"},"What to do:")," Record the new connection. The webhook identifies the customer and the application, but not the accounts or data types they authorized, so treat it as a signal to call ",(0,o.kt)("a",{parentName:"p",href:"#list-a-customers-connected-applications"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/application/list"))," for the full set of scopes. If you offer a consent portal, display this new connection so customers can view and manage it."),(0,o.kt)("h3",{id:"consent_revoked"},(0,o.kt)("inlineCode",{parentName:"h3"},"CONSENT_REVOKED")),(0,o.kt)("p",null,"Notifies you when a customer revokes an application's access, either through the application itself or via ",(0,o.kt)("a",{parentName:"p",href:"https://my.plaid.com"},"my.plaid.com")," (Plaid Portal). If you offer a consent portal, you need this webhook to catch the revocations that happen elsewhere; otherwise your portal will drift out of sync when customers disconnect through my.plaid.com or the application itself instead of through you."),(0,o.kt)("p",null,(0,o.kt)("strong",{parentName:"p"},"When it fires:")," After a customer or application revokes authorization."),(0,o.kt)("p",null,(0,o.kt)("strong",{parentName:"p"},"Payload includes:")," The customer (",(0,o.kt)("inlineCode",{parentName:"p"},"user_identifier"),"), application details, and who initiated the revocation (",(0,o.kt)("inlineCode",{parentName:"p"},"initiator"),": ",(0,o.kt)("inlineCode",{parentName:"p"},"DATA_ACCESS_PLATFORM"),", ",(0,o.kt)("inlineCode",{parentName:"p"},"INDIVIDUAL"),", or ",(0,o.kt)("inlineCode",{parentName:"p"},"DATA_RECIPIENT"),")."),(0,o.kt)("p",null,(0,o.kt)("strong",{parentName:"p"},"What to do:")," Mark the connection revoked in your own records and update your portal. Revocations that happen in Plaid Portal or in the application itself also land in ",(0,o.kt)("a",{parentName:"p",href:"#read-consent-activity-and-last-access-times"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/activity/list"))," as ",(0,o.kt)("inlineCode",{parentName:"p"},"PORTAL_UNLINK")," and ",(0,o.kt)("inlineCode",{parentName:"p"}
1,"ITEM_REMOVE")," entries."),(0,o.kt)("h2",{id:"best-practices"},"Best practices"),(0,o.kt)("ul",null,(0,o.kt)("li",{parentName:"ul"},"Call ",(0,o.kt)("a",{parentName:"li",href:"#revoke-an-applications-access"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/application/unlink"))," as soon as a customer disconnects an application, to keep the ecosystem in sync during revocations"),(0,o.kt)("li",{parentName:"ul"},"Implement the ",(0,o.kt)("inlineCode",{parentName:"li"},"CONSENT_REVOKED")," webhook if you offer a consent portal"),(0,o.kt)("li",{parentName:"ul"},"Set refresh token expiration to 13+ months (allows buffer for reauthorization)"),(0,o.kt)("li",{parentName:"ul"},"Direct users to ",(0,o.kt)("a",{parentName:"li",href:"https://my.plaid.com"},"my.plaid.com")," for self-service connection management")),(0,o.kt)("h2",{id:"testing-in-sandbox"},"Testing in Sandbox"),(0,o.kt)("p",null,"Point the same calls at ",(0,o.kt)("inlineCode",{parentName:"p"},"sandbox.plaid.com")," with your Sandbox secret. ",(0,o.kt)("a",{parentName:"p",href:"#exchange-a-customer-identifier-for-an-access-token"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/import"))," returns a working Sandbox access token, and ",(0,o.kt)("a",{parentName:"p",href:"#list-a-customers-connected-applications"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/application/list"))," and ",(0,o.kt)("a",{parentName:"p",href:"#read-consent-activity-and-last-access-times"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/activity/list"))," return fixture data for demo applications, so you can build and test your portal's rendering before you have live connections."),(0,o.kt)("p",null,"That fixture data is static: unlinking a demo application does not change what ",(0,o.kt)("a",{parentName:"p",href:"#list-a-customers-connected-applications"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/application/list"))," returns. To exercise the state changes instead, use dynamic Sandbox, which is enabled for most clients: connect a demo app, see it appear in ",(0,o.kt)("a",{parentName:"p",href:"#list-a-customers-connected-applications"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/application/list")),", unlink it, and see it move to ",(0,o.kt)("inlineCode",{parentName:"p"},"disconnected_applications"),". ",(0,o.kt)("inlineCode",{parentName:"p"},"/sandbox/item/application/seed")," connects a demo application to a customer, standing in for a customer completing your OAuth flow. If it returns ",(0,o.kt)("inlineCode",{parentName:"p"},"403 SANDBOX_SEEDING_NOT_ENABLED"),", dynamic Sandbox is off for your client — ask your Plaid solutions engineering team to enable it. ",(0,o.kt)("a",{parentName:"p",href:"#read-consent-activity-and-last-access-times"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/activity/list"))," always serves its fixture in Sandbox, dynamic Sandbox or not, so you can build your portal's activity view against it, but you won't see real activity until Production."),(0,o.kt)("h4",{id:"post-sandboxitemapplicationseed"},(0,o.kt)("inlineCode",{parentName:"h4"},"POST /sandbox/item/application/seed")),(0,o.kt)(h,{method:"POST",route:"/sandbox/item/application/seed",type:"request",mdxType:"Schema"},(0,o.kt)("pre",null,(0,o.kt)("code",{parentName:"pre",className:"language-bash"},'curl -X POST \'https://sandbox.plaid.com/sandbox/item/application/seed\' \\\n  -H \'Content-Type: application/json\' \\\n  -d \'{\n    "client_id": "<client_id>",\n    "secret": "<sandbox_secret>",\n    "access_token": "<access_token>",\n    "application_id": "c048eab9-a70a-42fe-94e9-35980d0a965c"\n  }\'\n'))),(0,o.kt)(h,{method:"POST",route:"/sandbox/item/application/seed",type:"response",mdxType:"Schema"}),(0,o.kt)("p",null,"Plaid provides three demo applications to connect:"),(0,o.kt)("table",null,(0,o.kt)("thead",{parentName:"table"},(0,o.kt)("tr",{parentName:"thead"},(0,o.kt)("th",{parentName:"tr",align:null},"Demo application"),(0,o.kt)("th",{parentName:"tr",align:null},"application_id"))),(0,o.kt)("tbody",{parentName:"table"},(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},"Budget Works"),(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"c048eab9-a70a-42fe-94e9-35980d0a965c"))),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},"Incremental Investing"),(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"36b78a27-ebbb-417b-a7b7-351734d2f8bd"))),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},"My Peer Payer"),(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"3f952a96-b6f9-4f09-a1f7-1a474cb128f6"))))),(0,o.kt)("h2",{id:"errors"},"Errors"),(0,o.kt)("p",null,"These endpoints return Plaid's standard ",(0,o.kt)("a",{parentName:"p",href:"/docs/errors/"},"error object")," (",(0,o.kt)("inlineCode",{parentName:"p"},"error_type"),", ",(0,o.kt)("inlineCode",{parentName:"p"},"error_code"),", ",(0,o.kt)("inlineCode",{parentName:"p"},"error_message"),", ",(0,o.kt)("inlineCode",{parentName:"p"}
1,"display_message"),", ",(0,o.kt)("inlineCode",{parentName:"p"},"request_id"),"). Log ",(0,o.kt)("inlineCode",{parentName:"p"},"request_id")," when you encounter an error to speed up troubleshooting with Plaid."),(0,o.kt)(s.Z.FullWidth,null,(0,o.kt)("div",{className:"status-error-code-table"},(0,o.kt)("table",null,(0,o.kt)("thead",{parentName:"table"},(0,o.kt)("tr",{parentName:"thead"},(0,o.kt)("th",{parentName:"tr",align:null},"Status"),(0,o.kt)("th",{parentName:"tr",align:null},"error_code"),(0,o.kt)("th",{parentName:"tr",align:null},"Returned when"))),(0,o.kt)("tbody",{parentName:"table"},(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},"400"),(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"MISSING_FIELDS")),(0,o.kt)("td",{parentName:"tr",align:null},"A required field is absent from the request body")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},"400"),(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"INVALID_USER_AUTH")),(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"user_auth.user_id")," is missing or is not a non-empty string")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},"400"),(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"ITEM_NOT_CREATED_BY_ITEM_IMPORT")),(0,o.kt)("td",{parentName:"tr",align:null},"The ",(0,o.kt)("inlineCode",{parentName:"td"},"access_token")," belongs to an Item created some other way than ",(0,o.kt)("a",{parentName:"td",href:"#exchange-a-customer-identifier-for-an-access-token"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/import")))),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},"400"),(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"ITEM_IMPORT_INACTIVE_ACCESS_TOKEN")),(0,o.kt)("td",{parentName:"tr",align:null},"The ",(0,o.kt)("inlineCode",{parentName:"td"},"access_token")," is no longer active")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},"400"),(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"FI_PARTNER_INSTITUTION_NOT_SET_UP")),(0,o.kt)("td",{parentName:"tr",align:null},"Your institution is not configured for Permissions Manager — contact your Plaid solutions engineering team")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},"404"),(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"CONNECTED_APPLICATION_NOT_FOUND")),(0,o.kt)("td",{parentName:"tr",align:null},"The ",(0,o.kt)("inlineCode",{parentName:"td"},"application_id")," and ",(0,o.kt)("inlineCode",{parentName:"td"},"access_token")," pair has no active connection")),(0,o.kt)("tr",{parentName:"tbody"},(0,o.kt)("td",{parentName:"tr",align:null},"404"),(0,o.kt)("td",{parentName:"tr",align:null},(0,o.kt)("inlineCode",{parentName:"td"},"NO_CONNECTED_APPLICATIONS")),(0,o.kt)("td",{parentName:"tr",align:null},"The customer has no connections, for the older integrations that receive a ",(0,o.kt)("inlineCode",{parentName:"td"},"404")," for this rather than a ",(0,o.kt)("inlineCode",{parentName:"td"},"200"))))))),(0,o.kt)("p",null,"A customer with no connections is a normal state, not a failure: ",(0,o.kt)("a",{parentName:"p",href:"#list-a-customers-connected-applications"},(0,o.kt)("inlineCode",{parentName:"a"},"/item/application/list"))," returns ",(0,o.kt)("inlineCode",{parentName:"p"},"200")," with an empty ",(0,o.kt)("inlineCode",{parentName:"p"},"applications")," array. If you integrated before February 2022, you may receive ",(0,o.kt)("inlineCode",{parentName:"p"},"404 NO_CONNECTED_APPLICATIONS")," instead; render either as an empty portal."),(0,o.kt)("p",null,"Unlinking an application whose access has already ended returns ",(0,o.kt)("inlineCode",{parentName:"p"},"204")," with ",(0,o.kt)("inlineCode",{parentName:"p"},"APPLICATION_ALREADY_UNLINKED"),". Treat it as success — the revocation you asked for is already in effect — which makes retries safe.")))}y.isMDXComponent=!0;var f=[{id:"overview",level:2,title:"Overview"},{id:"authentication",level:2,title:"Authentication"},{id:"customer-identifiers-and-access-tokens",level:2,title:"Customer identifiers and access tokens"}
1,{id:"exchange-a-customer-identifier-for-an-access-token",level:3,title:"Exchange a customer identifier for an access token"},{id:"post-itemimport",level:4,title:"POST /item/import"},{id:"endpoints",level:2,title:"Endpoints"},{id:"list-a-customers-connected-applications",level:3,title:"List a customer's connected applications"},{id:"post-itemapplicationlist",level:4,title:"POST /item/application/list"},{id:"reading-the-scopes-object",level:4,title:"Reading the scopes object"},{id:"read-consent-activity-and-last-access-times",level:3,title:"Read consent activity and last access times"},{id:"post-itemactivitylist",level:4,title:"POST /item/activity/list"},{id:"revoke-an-applications-access",level:3,title:"Revoke an application's access"},{id:"post-itemapplicationunlink",level:4,title:"POST /item/application/unlink"},{id:"webhooks",level:2,title:"Webhooks"},{id:"authorization_granted",level:3,title:"AUTHORIZATION_GRANTED"},{id:"consent_revoked",level:3,title:"CONSENT_REVOKED"},{id:"best-practices",level:2,title:"Best practices"},{id:"testing-in-sandbox",level:2,title:"Testing in Sandbox"}
1,{id:"post-sandboxitemapplicationseed",level:4,title:"POST /sandbox/item/application/seed"},{id:"errors",level:2,title:"Errors"}];g.tableOfContents=f,y.layoutProps=g,y.layout=function(e){return(0,o.kt)(r.Z,e)}},99525:function(e,t,a){(window.__NEXT_P=window.__NEXT_P||[]).push(["/core-exchange/docs/consent-management/permissions-manager",function(){return a(19583)}])},43402:function(e){e.exports={page:"core-exchange_page__hVAuI"}}},function(e){e.O(0,[49774,86898,26736,29622,12291,22359,68239,79255,92888,40179],function(){return e(e.s=99525)}),_N_E=e.O()}]);
2//# sourceMappingURL=permissions-manager-347d5cc5ed740429.js.map

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.