Simple Solutions - OAuth and Membership data

Okay, TBH, I’m hunting for a simple solution to this particular problem that doesn’t involve me datamining API documentation or spending 18 hours coding. (I’m not lazy, I’m just annoyed at having to rebuild the wheel every time I want to do something)

What I’d like is to send folks off to Patreon for Oauth, and on return, be able to get pledge info from them about my campaign.

So simple 2 steps. Click a link to “Connect to Patreon”, and on return be able to get user data (to setup a local account) and Pledge info.

Working in PHP, building a custom tool for boardgame devs, and I want my Patreons to get special benefits.

Thanks,

–D

I spent two days on it and I finally got it. I made a small service which does something after the authentication. It’s JavaScript, but I’ll just post the general flow for you.

This assumes you have a server which can handle redirect and registered client and it has APIs selected to V2. If you making some desktop software you must run a localhost webserver at some port to handle the redirect. Make sure to set up redirect URL in Pateron’ Client setting in the Patreon.

So, it’s a 3 step thing. First you open a browser window with similar link

window.open("https://www.patreon.com/oauth2/authorize?client_id=l4YlAvUnfrNrTk9uiupqgnAb3PxDIScWv_FU2f5GVrCiyxEIJCeALuxzLB9R335s&scope=identity%20identity.memberships&allow_signup=false&response_type=code&redirect_uri=https://supporters.solar2d.workers.dev/patreon-callback", "Login with Patreon", 'toolbar=no, menubar=no, width=400, height=600')

This is when you hand stuff to patreon. Window is presented with login and asking for user to provide you information. This is where I stuck for longest, because for some silly reason you must provide both identity and identity.memberships scopes. Second one does not assume first one.

Then Flow is goin on your server. After user is done and authorised (or not) Patreon would make a redirec to your callback. This is where your server must handle things.

Server would redirect with the ‘code’ get parameter. You need to exchange code for access token. This is done with a post request

        const code = url.searchParams.get("code");
        if (!code)
            return new Response("No valid authentication code", {
                status: 500,
            });
        const body = {
            client_id: patreonClientId,
            client_secret: patreonClientSecret,
            code: code,
            grant_type: "authorization_code",
            redirect_uri: "https://supporters.solar2d.workers.dev/patreon-callback",
        };
        const init = {
            body: new URLSearchParams(Object.entries(body)).toString(),
            method: "POST",
            headers: {
                "content-type": "application/x-www-form-urlencoded",
                accept: "application/json",
            },
        };
        const response = await fetch("https://www.patreon.com/api/oauth2/token", init);
        const results = await response.json();
        if (results.error)
            return new Response("Patreon Error: " + (results.error_description || results.error), {
                status: 500,
            });

Note that exchanging Code for token requires a x-www-form-urlencoded data. I use java script built in facilities for that, but it isn’t hard just to replace part of the string with your code.
After latest request is finished, you have access token in results.access_token and you can use it to make API calls.
I’m only want to verify if user is supporting my patreon. I make following request for it:

        const ptResponse = await fetch("https://www.patreon.com/api/oauth2/v2/identity?include=memberships.campaign&fields%5Bmember%5D=patron_status", {
            method: "GET",
            headers: {
                accept: "application/json",
                Authorization: `Bearer ${results.access_token}`,
            },
        });
        if(ptResponse.status != 200) return ptResponse;
        try{
            const ptData = await ptResponse.json();
            const sponsorships = ptData.included.filter(e => e.type === 'member').filter(e => e.attributes.patron_status === "active_patron").filter(e => e.relationships.campaign.data.id === patreonCampaignId)
            results.patreonOk = sponsorships.length > 0;
        } catch {}

Note that I use token in authorization header. memberships.campaign is undocumented for some reason, but you need it to get campaign IDs (campaign is a creator’s page). Otherwise you’ll just get pledges without knowing what user is supporting (I know, right?).

Results have different fields, but you need included array, and only entries from it which have type field set to member. Then attributes would contain for once documented Member fields (note that I requested only patron status, but you can request others, just add them to request comma separated).
Here I filter only for active patrons (attributes.patron_status === "active_patron"). Last step for me is to only filter for my own campaign. Note that campaign is not stored in Member attributes, it is in relationships.campaign.data.id.
So, if you have entry with relation to your campaign and with active patron status, user gave you some money.

If you need to store token, note that it will expire, and you got to renew it time to time. But this is outside of the scope of my post.

Good luck!
Hope it helps.

P.S. Easiest way to find your campaign ID is to open your page in browser, right click in the middle of the page and select “View page source”. Then search for “campaign/” and you should find a bunch of links similar to campaign/3949580/. That number is your campaign ID.

Thanks, I’ve gotted about this far last night, after slamming my head against the code till 2 am. (So much for not wanting to invest hours)

I’m now in the weeds (AGAIN, dammit, just what I wanted to avoid) trying to pull campaign data and marry it up to tiers. It’s frustrating, because the data isn’t easy to get, but it’s more useful than what they do deliver.

I mean, really, all I want is given my clientID/key/etc for Oauth to return information about the logged in client, specifically, what is their status, are they supporting my campaign, at what tier. What I don’t need to know is what other campaigns they’re supporting (none of my business) or the current number of cents, because if I pause my campaign, that drops to 0 for the paused month and that means I can’t tell what tier they’re in even if they’re active. That’s a problem if I’m tying benefits on a tool or an app to their Patreon tier.

So, please, question for the experts. A simple path forward. When a user logs in via oauth, I need the following:

  • User Details (name, email, etc) DONE - gotten with the identity, and identity.email scopes
  • Campaign Status (Are they subscribing to my campaign?) NOT DONE I need to hardcode my campaign’s UUID which is fine, even tho not ideal, but the campaign ID I’ve got from pulling from the source is not the same as the UUID’s provided in the membership data from the API, so no way to match that up.
  • Support Tier NOT DONE Still trying to find out how they’re supporting.

Addendum. Pulling the campaign details doesn’t help either, as while I get a list of tiers, it only includes ids and no data. I can’t tell one tier from another with what’s coming in, and it kind of defeats the purpose if I need to go back and request each and every tier’s details every time. I’ll do it, but man, not a good way to return the data.

1 Like

You can just check out the PHP lib:

Its designed precisely for that purpose and the example does precisely that. You should be able to easily translate the logic it has to the language/framework that you are using.

I answered to first part in previous post, not sure why you overlooked it. There is no single API call to do what you want to do and return true/false answer. You have to parse the reply to the query and match your campaign and tier IDs, and see if user supports specific tier of specific campaign. Also, it is possible that you need to match only tiers because they seem to have unique IDs.

Parse answer to this query:

https://www.patreon.com/api/oauth2/v2/identity?include=memberships.campaign,memberships.currently_entitled_tiers&fields%5Bmember%5D=patron_status,email

Here is full sample reply, relations would contain tier information you need, look for Member type like I wrote in previous post

{
    "data": {
        "attributes": {},
        "id": "30572273",
        "relationships": {
            "memberships": {
                "data": [
                    {
                        "id": "38ce14d8-7e05-4901-a56e-cca9329b1a8c",
                        "type": "member"
                    }
                ]
            }
        },
        "type": "user"
    },
    "included": [
        {
            "attributes": {
                "patron_status": "active_patron"
            },
            "id": "38ce14d8-7e05-4901-a56e-cca9329b1a8c",
            "relationships": {
                "campaign": {
                    "data": {
                        "id": "4976214",
                        "type": "campaign"
                    },
                    "links": {
                        "related": "https://www.patreon.com/api/oauth2/v2/campaigns/4976214"
                    }
                },
                "currently_entitled_tiers": {
                    "data": [
                        {
                            "id": "5592833",
                            "type": "tier"
                        }
                    ]
                }
            },
            "type": "member"
        },
        {
            "attributes": {},
            "id": "4976214",
            "type": "campaign"
        },
        {
            "attributes": {},
            "id": "5592833",
            "type": "tier"
        }
    ],
    "links": {
        "self": "https://www.patreon.com/api/oauth2/v2/user/30572273"
    }
}

What you need is to match the type itself (type =="member") attributes.patron_status, then relationships.campaign.data.id and relationships.currently_entitled_tiers.data.id.
If it is just for you I suggest hardcoding IDs.

It’s not that I overlooked your response, but the API wasn’t comprehensive. To get the answers to what I’d guess is a common need, we have to make multiple API calls. One to get the user OAuth data, one to get the user’s memberships, one to get campaign data, and then one to get the campaign memberships. Then you need to sort through all this data, discard the stuff that you didn’t need to get (such as all the other users that are members), and tie it together with id’s that in some cases look like sequentials, and in others look like UUIDs.

I was hoping for a basic, SIMPLE, call. One that when going against the clientID/key for my campaign, would return user data that contains the membership info pertinent to the campaign. Instead it’s a multi step, multi call process with a lot of post processing, and a lot of hours spent building out those steps that I could have used for other things.

Check out the calls which WP plugin uses to get user identity and some membership info in the same call:

fetch_user function has it.

I’ve solved the problem by doing the one thing I didn’t want to do (spend hours solving the problem that should have been part and parcel to the api), and have put together a composer library to handle this in the future. Unfortunately, the package will only work until the API changes again.

https://packagist.org/packages/rcsi/wrapper-patreonoauth