BakedBrie docs

Recipe: connect any API with OAuth

Some services do not hand out API keys: you sign in with OAuth 2.0 instead. BakedBrie connects them through your own OAuth app at the service (its client id and client secret). Steps, templates, preview and checks work exactly as in Connect any API, so read that page first. Only the sign-in is different.

How it works:

  • The definition's auth is {"type": "oauth2", ...}. The destination's credential is the OAuth app's client secret. BakedBrie sends it only to the service's token and revocation URLs, never to the API.
  • Authorization code (grant: "authorization_code"): a person approves once, in a browser. BakedBrie keeps the tokens sealed, never shows them, and adds the access token to API calls to the base URL's host only.
  • Client credentials (grant: "client_credentials", machine to machine): no browser step. BakedBrie gets a token with the client id and secret.
  • An agent can hand over the sign-in link but can never finish a sign-in: that happens only in the browser of the person who connected the destination, or a workspace owner or admin.
  • Changing the base URL or auth later needs the client secret again and a new sign-in, so a sign-in never follows the definition to a new place.

Before you run it

  1. At the service, create an OAuth app and register this exact redirect URL. It must match character for character:
   https://app.bakedbrie.com/settings/destinations/oauth/callback
  1. Note the app's client id. Put its client secret in an environment variable in your own shell, for example export LINKEDIN_CLIENT_SECRET=.... Never paste it into the chat.
  2. From the service's API docs, find: the API base URL, the authorize URL, the token URL, the revocation URL (if it has one), the scopes you need, whether it supports PKCE, and whether it wants the client id and secret as HTTP Basic or as form fields.
  3. whoami must show destinations, api_workflow and custom_oauth on.

The prompt

In BakedBrie, make finished cards on the board "{{BOARD_NAME}}" send to {{SERVICE_NAME}} through a custom API destination that signs in with OAuth. Use only the BakedBrie MCP tools. Start with whoami and stop if destinations, api_workflow or custom_oauth is off. First read the docs pages /docs/recipes/connect-any-api, /docs/recipes/connect-any-api-oauth and /docs/concepts (section "Custom API steps and templates") with read_docs.

The service: API base URL {{BASE_URL}}, OAuth app client id {{CLIENT_ID}}, grant {{GRANT}}, authorize URL {{AUTHORIZE_URL}}, token URL {{TOKEN_URL}}, revocation URL {{REVOCATION_URL_OR_NONE}}, scopes {{SCOPES}}. The client secret is in my environment variable {{ENV_VAR}}. A harmless GET that proves the sign-in works: {{CHECK_PATH}}.
What a finished card should become: {{WHAT_DELIVER_DOES}}

1. Find the board with list_boards.
2. Call prepare_secret with kind oauth_client_secret, purpose destination and env_var {{ENV_VAR}}. Run the command it returns in my shell exactly as given, then use the drop_id. Never print the secret.
3. Write the webhook definition with auth {"type": "oauth2", ...} from the recipe. Call manage_destination with action preview and that webhook (a draft), show me each request, and fix every problem it lists. A preview sends nothing.
4. When I agree, create it with manage_destination: action create, name "{{SERVICE_NAME}}", the webhook, credential {"drop_id": "<the drop id>"}.
5. For authorization_code: give me oauth.consent_url from the answer. I open it in the browser where I am signed in to BakedBrie, approve at {{SERVICE_NAME}}, and click Finish connecting. Then call manage_destination get every few seconds until oauth_grant.state is connected, or tell me oauth_grant.error_code if it is failed. For client_credentials: call get until oauth_grant.state is connected.
6. Check the connection: manage_destination action test_call with the destination_id and path {{CHECK_PATH}}. Read it with test_call_id until state is done or failed, and tell me the status.
7. Make it the board's default destination with manage_destination action set_board_default.
8. Read it back with manage_destination action get and tell me, in plain words, what each finished card will send.

The OAuth auth block

FieldWhat it is
typeAlways oauth2.
grantauthorization_code (a person approves in the browser) or client_credentials (machine to machine).
client_idThe OAuth app's client id. Not a secret: the secret goes in credential.
authorize_urlAuthorization code only: where the person approves. https, no query.
token_urlWhere BakedBrie gets and refreshes tokens. https, no query.
revocation_urlOptional. Where BakedBrie asks the service to revoke the sign-in when the destination is turned off (RFC 7009).
scopesThe scopes to ask for, for example ["openid", "profile"]. Default none. At most 30.
client_authHow the client id and secret go to the token URL: basic (HTTP Basic, the default) or body (form fields).
pkceAuthorization code only: S256 (the default) or off for a service that does not support PKCE.
issuerAuthorization code only, optional. When set, the service must send back this iss with the sign-in (RFC 9207).
extra_authorize_paramsAuthorization code only, optional: access_type (offline or online), prompt (consent, select_account, login or none) and include_granted_scopes (true or false). Nothing else.
header, prefixWhere the access token goes on API calls. Default Authorization and Bearer .
providerlinkedin, sprout_social or google: the URLs and the grant must then be exactly the ones in the table below. other, or left out: any https URLs.

A named provider must use exactly these URLs and one of these grants. Use the client_auth and pkce shown too: they are what each service documents.

ProviderGrantsAuthorize URLToken URLRevocation URLclient_authpkce
linkedinauthorization_codehttps://www.linkedin.com/oauth/v2/authorizationhttps://www.linkedin.com/oauth/v2/accessTokennonebodyoff
sprout_socialauthorization_code, client_credentialshttps://identity.sproutsocial.com/oauth2/84e39c75-d770-45d9-90a9-7b79e3037d2c/v1/authorizehttps://identity.sproutsocial.com/oauth2/84e39c75-d770-45d9-90a9-7b79e3037d2c/v1/tokenhttps://identity.sproutsocial.com/oauth2/84e39c75-d770-45d9-90a9-7b79e3037d2c/v1/revokebodyS256
googleauthorization_codehttps://accounts.google.com/o/oauth2/v2/authhttps://oauth2.googleapis.com/tokenhttps://oauth2.googleapis.com/revokebodyS256

For google, an issuer, when set, must be https://accounts.google.com. Google returns a refresh token only with access_type offline; prompt consent makes it ask again, so a repeat sign-in gets one too: "extra_authorize_params": {"access_type": "offline", "prompt": "consent"}.

After the sign-in

manage_destination get returns oauth_grant (never a token):

oauth_grant.stateMeaning
exchange_pendingThe person approved. BakedBrie is finishing the sign-in with the service. Read again in a few seconds.
connectedDeliveries send with this sign-in.
reconnect_neededThe service stopped accepting it. Deliveries fail with OAUTH_RECONNECT_NEEDED until the person signs in again with oauth.consent_url (the web app shows Sign in again).
failedThe sign-in did not finish. error_code says why. Check the client id, the secret, the scopes and the redirect URL, then sign in again.
revoking, revokedTurned off. revocation says whether the service confirmed (confirmed, unconfirmed, or not_supported when there is no revocation URL).

For an authorization code destination, create, get and complete_oauth also return oauth: consent_url (a BakedBrie link; once opened, the sign-in must finish within 10 minutes), redirect_uri (the URL the OAuth app must list) and next (what to tell the person).

To stop using a sign-in while keeping the destination, the person turns it off in the BakedBrie web app (the destination's settings). An API token cannot disconnect it. manage_destination action disable turns off the whole destination and its sign-in.

Worked example: LinkedIn

This posts each finished card to the LinkedIn feed of the person who signs in. It needs two self-serve LinkedIn products and three scopes.

  1. At LinkedIn Developers, create an app. On its Products tab, add Share on LinkedIn (scope w_member_social) and Sign In with LinkedIn using OpenID Connect (scopes openid and profile).
  2. On its Auth tab, add the redirect URL https://app.bakedbrie.com/settings/destinations/oauth/callback, and copy the client id. In your shell: export LINKEDIN_CLIENT_SECRET=... (the app's client secret).
  3. prepare_secret with {"kind": "oauth_client_secret", "purpose": "destination", "env_var": "LINKEDIN_CLIENT_SECRET"}, and run the command it returns.
  4. manage_destination action preview, then create with this webhook and credential {"drop_id": "<the drop id>"}:
   {
     "base_url": "https://api.linkedin.com",
     "auth": {
       "type": "oauth2",
       "grant": "authorization_code",
       "provider": "linkedin",
       "client_id": "YOUR_LINKEDIN_CLIENT_ID",
       "authorize_url": "https://www.linkedin.com/oauth/v2/authorization",
       "token_url": "https://www.linkedin.com/oauth/v2/accessToken",
       "client_auth": "body",
       "pkce": "off",
       "scopes": ["openid", "profile", "w_member_social"]
     },
     "headers": {"Linkedin-Version": "202609", "X-Restli-Protocol-Version": "2.0.0"},
     "actions": {
       "deliver": {
         "steps": [
           {"name": "me", "method": "GET", "path": "/v2/userinfo", "save": {"sub": "$.sub"}, "require": ["sub"]},
           {
             "name": "post",
             "path": "/rest/posts",
             "body": {
               "author": "urn:li:person:{{steps.me.sub}}",
               "commentary": "{{result.captions.platform_captions.LINKEDIN}}",
               "visibility": "PUBLIC",
               "distribution": {"feedDistribution": "MAIN_FEED", "targetEntities": [], "thirdPartyDistributionChannels": []},
               "lifecycleState": "PUBLISHED",
               "isReshareDisabledByAuthor": false
             }
           }
         ]
       }
     }
   }

A preview with the sample card lists TEMPLATE_INVALID for the missing LINKEDIN caption (see below). Preview with the card_id of a finished card whose captions have one to see the real post.

  1. Give the person oauth.consent_url from the answer. They open it in the browser where they are signed in to BakedBrie, approve on LinkedIn, and click Finish connecting. get then shows oauth_grant.state connected.
  2. test_call with path /v2/userinfo: status 200, and the snippet shows the person's name. The token is never shown.
  3. set_board_default, then move a card to Done.

What each part does, from LinkedIn's docs:

  • GET /v2/userinfo (Sign In with LinkedIn using OpenID Connect) answers the member's sub. The post's author is urn:li:person:<sub>.
  • POST /rest/posts (the Posts API) needs the Linkedin-Version header (a supported version, YYYYMM; 202609 is the latest as of September 2026, and each version is supported for at least a year) and X-Restli-Protocol-Version: 2.0.0. PUBLISHED is the only lifecycleState LinkedIn accepts when a post is created, so a post goes live at once; there are no drafts.
  • LinkedIn reads commentary as its "little" text format, where | { } @ [ ] ( ) < > # \ * _ ~ are reserved and must be escaped with a backslash. BakedBrie templates have no filter for that, so the example sends the card's LINKEDIN platform caption: tell the board's agent to write it in the bakedbrie-captions block with those characters escaped. A card without one stops before anything is sent.

Limits to know:

  • LinkedIn sign-ins last 60 days, and LinkedIn gives refresh tokens only to a limited set of partner apps. After 60 days the destination shows reconnect_needed (Sign in again in the web app): the person opens oauth.consent_url again.
  • LinkedIn answers a new post with 201 and puts the post id in a response header, which BakedBrie does not read. The receipt shows the step succeeded with status 201 and no post id, so the post step has no require.
  • Finding a member's own posts needs a restricted LinkedIn permission, so there is no reconcile. If a post's answer is lost, the delivery is outcome unknown and is never resent: look at the LinkedIn feed before you post it again by hand.
  • LinkedIn lists no revocation URL. Turning the destination off deletes BakedBrie's copy of the sign-in; the person can also remove the app's access in their LinkedIn settings.
  • Posting as a company page needs the w_organization_social permission, which this example does not cover.
  • Share on LinkedIn allows 150 requests per member per day.

Sprout Social

The Sprout Social API is part of Sprout's Advanced plan. It works two ways:

  • An API token: send it as Authorization: Bearer <token>, so use the plain Connect any API recipe with header auth. No OAuth needed.
  • OAuth machine to machine: client_credentials with scope organization_id. No browser step.

Sprout's publishing API creates drafts only ("is_draft": true is required): a person finishes each post in Sprout. Find your customer id with GET /v1/metadata/client and your profile ids with GET /v1/<customer id>/metadata/customer (a test_call shows them). The API allows 60 requests per minute and 250,000 per month.

{
  "base_url": "https://api.sproutsocial.com",
  "auth": {
    "type": "oauth2",
    "grant": "client_credentials",
    "provider": "sprout_social",
    "client_id": "YOUR_SPROUT_CLIENT_ID",
    "token_url": "https://identity.sproutsocial.com/oauth2/84e39c75-d770-45d9-90a9-7b79e3037d2c/v1/token",
    "revocation_url": "https://identity.sproutsocial.com/oauth2/84e39c75-d770-45d9-90a9-7b79e3037d2c/v1/revoke",
    "client_auth": "body",
    "scopes": ["organization_id"]
  },
  "slots": {"timezone": "America/New_York", "daily_times": ["10:00"], "include_weekends": false},
  "actions": {
    "deliver": {
      "steps": [
        {"name": "slot", "kind": "reserve_slot", "save": {"slot": "$.slot"}, "receipt": ["slot"]},
        {
          "name": "draft",
          "path": "/v1/YOUR_CUSTOMER_ID/publishing/posts",
          "body": {
            "group_id": 55667788,
            "customer_profile_ids": [2345],
            "is_draft": true,
            "text": "{{result.captions.caption}}",
            "delivery": {"scheduled_times": ["{{steps.slot.slot}}"], "type": "SCHEDULED"}
          },
          "save": {"post_id": "$.data[0].internal.publishing.publishing_post_id"},
          "receipt": ["post_id"],
          "external_id": "post_id"
        }
      ]
    }
  }
}

Replace YOUR_CUSTOMER_ID, group_id and customer_profile_ids with your own. Check the body against Sprout's publishing docs before the first send.

What to expect

  • Deliveries, receipts, redeliver and preview work as in Connect any API. A preview shows the token header as Bearer •••• (from sign-in).
  • When the access token runs out, BakedBrie refreshes it before the first step (when the service gave a refresh token), never in the middle of a delivery. A 401 in the middle fails that delivery without sending again; the next one refreshes first.
  • Every code: Refusals.

View as Markdown