Why is Test Payload Data Structure Different from Real Payloads?

Okay, really. This is more of a rant than anything else, but it’s also a cry for change.

When sending a test webhook trigger event via the /portal/registration/register-webhooks Send Test link as shown below:

The structure of the actual payload is vastly different than if it were a real payload triggered by the same type of event (in this case, the only event I currently care about, members:pledge:create).

Here’s the test data structure:

{
    "data": {
        "attributes": {
            "access_expires_at": null,
            "campaign_currency": "USD",
            "campaign_lifetime_support_cents": 12345,
            "campaign_pledge_amount_cents": 200,
            "full_name": "FIRST LAST",
            "is_follower": false,
            "last_charge_date": "2014-04-01T00:00:00.000+00:00",
            "last_charge_status": "Paid",
            "lifetime_support_cents": 12345,
            "patron_status": "active_patron",
            "pledge_amount_cents": 200,
            "pledge_relationship_start": "2014-03-14T00:00:00.000+00:00"
        },
        "id": null,
        "relationships": {
            "address": {
                "data": {
                    "id": "********",
                    "type": "address"
                },
                "links": {
                    "related": "https://www.patreon.com/api/addresses/*******"
                }
            },
            "campaign": {
                "data": {
                    "id": "******",
                    "type": "campaign"
                },
                "links": {
                    "related": "https://www.patreon.com/api/campaigns/******"
                }
            },
            "user": {
                "data": {
                    "id": "***********",
                    "type": "user"
                },
                "links": {
                    "related": "https://www.patreon.com/api/user/**********"
                }
            }
        },
        "type": "member"
    },
    "included": [{
        "attributes": {
            "addressee": "FIRST LAST",
            "city": "CITY",
            "country": "US",
            "created_at": "2017-05-10T18:17:12.000+00:00",
            "line_1": "ADDRESS LINE ONE",
            "line_2": null,
            "phone_number": null,
            "postal_code": "ZIP",
            "state": "2-DIGIT STATE",
            "updated_at": "2018-08-07T01:19:15.715+00:00"
        },
        "id": "*******",
        "type": "address"
    }, {
        "attributes": {
            "avatar_photo_url": "[URL]",
            "campaign_pledge_sum": 0,
            "cover_photo_url": "[URL]",
            "cover_photo_url_sizes": {
                "large": "[URL]",
                "medium": "[URL]",
                "small": "[URL]"
            },
            "created_at": "2021-01-12T02:01:27.000+00:00",
            "creation_count": 0,
            "creation_name": "[CREATION NAME]",
            "currency": "USD",
            "discord_server_id": null,
            "display_patron_goals": false,
            "earnings_visibility": "public",
            "image_small_url": "[URL TO SMALL IMAGE]",
            "image_url": "[URL TO IMAGE]",
            "is_charge_upfront": false,
            "is_charged_immediately": false,
            "is_monthly": true,
            "is_nsfw": false,
            "is_plural": false,
            "main_video_embed": null,
            "main_video_url": null,
            "name": "[FIRST] [LAST]",
            "one_liner": null,
            "outstanding_payment_amount_cents": 0,
            "patron_count": 0,
            "pay_per_name": "month",
            "pledge_sum": 0,
            "pledge_sum_currency": "USD",
            "pledge_url": "/join/*******",
            "published_at": "2021-01-12T02:05:13.000+00:00",
            "summary": "[PATREON PAGE SUMMARY]",
            "thanks_embed": null,
            "thanks_msg": null,
            "thanks_video_url": null,
            "url": "https://www.patreon.com/********"
        },
        "id": "*******",
        "relationships": {
            "creator": {
                "data": {
                    "id": "*******",
                    "type": "user"
                },
                "links": {
                    "related": "https://www.patreon.com/api/user/UEHDGKU"
                }
            },
            "goals": {
                "data": []
            },
            "rewards": {
                "data": [{
                    "id": "-1",
                    "type": "reward"
                }, {
                    "id": "0",
                    "type": "reward"
                }]
            }
        },
        "type": "campaign"
    }, {
        "attributes": {
            "about": null,
            "apple_id": null,
            "can_see_nsfw": true,
            "created": "2017-03-05T18:03:14.000+00:00",
            "default_country_code": null,
            "discord_id": "[Discord USER ID]",
            "email": "EMAIL@ADDRESS.TLD",
            "facebook": null,
            "facebook_id": null,
            "first_name": "[FIRST NAME]",
            "full_name": "[FIRST] [LAST]",
            "gender": 0,
            "google_id": null,
            "has_password": true,
            "image_url": "https://c8.patreon.com/2/200/*******",
            "is_deleted": false,
            "is_email_verified": true,
            "is_nuked": false,
            "is_suspended": false,
            "last_name": "[LAST NAME]",
            "patron_currency": "USD",
            "social_connections": {
                "deviantart": null,
                "discord": {
                    "scopes": ["guilds", "guilds.join", "identify"],
                    "url": null,
                    "user_id": "************"
                },
                "facebook": null,
                "google": null,
                "instagram": null,
                "reddit": null,
                "spotify": null,
                "twitch": null,
                "twitter": null,
                "youtube": null
            },
            "thumb_url": "https://c8.patreon.com/2/200/********",
            "twitch": null,
            "twitter": null,
            "url": "https://www.patreon.com/*********",
            "vanity": "GoMortonGaming",
            "youtube": null
        },
        "id": "********",
        "relationships": {
            "campaign": {
                "data": {
                    "id": "*********",
                    "type": "campaign"
                },
                "links": {
                    "related": "https://www.patreon.com/api/campaigns/*******"
                }
            }
        },
        "type": "user"
    }, {
        "attributes": {
            "amount": 0,
            "amount_cents": 0,
            "created_at": null,
            "description": "Everyone",
            "patron_currency": "USD",
            "remaining": 0,
            "requires_shipping": false,
            "url": null,
            "user_limit": null
        },
        "id": "-1",
        "type": "reward"
    }, {
        "attributes": {
            "amount": 1,
            "amount_cents": 1,
            "created_at": null,
            "description": "Patrons Only",
            "patron_currency": "USD",
            "remaining": 0,
            "requires_shipping": false,
            "url": null,
            "user_limit": null
        },
        "id": "0",
        "type": "reward"
    }],
    "links": {
        "self": "https://www.patreon.com/api/members/None"
    }
}

And here is the real payload received by an actual patron pledging.

{
    "data": {
        "attributes": {
            "campaign_lifetime_support_cents": 0,
            "currently_entitled_amount_cents": 100,
            "email": "[EMAIL ADDRESS]",
            "full_name": "First Last",
            "is_follower": false,
            "last_charge_date": null,
            "last_charge_status": null,
            "lifetime_support_cents": 0,
            "next_charge_date": "2021-04-01T07:00:00.000+00:00",
            "note": "",
            "patron_status": "active_patron",
            "pledge_cadence": 1,
            "pledge_relationship_start": "2021-03-31T17:08:01.311+00:00",
            "will_pay_amount_cents": 100
        },
        "id": "*-*-****-****-************",
        "relationships": {
            "address": {
                "data": null
            },
            "campaign": {
                "data": {
                    "id": "*****",
                    "type": "campaign"
                },
                "links": {
                    "related": "https://www.patreon.com/api/oauth2/v2/campaigns/*****"
                }
            },
            "currently_entitled_tiers": {
                "data": []
            },
            "user": {
                "data": {
                    "id": "*****",
                    "type": "user"
                },
                "links": {
                    "related": "https://www.patreon.com/v2"
                }
            }
        },
        "type": "member"
    },
    "included": [{
        "attributes": {
            "created_at": "2021-01-12T02:01:27.000+00:00",
            "creation_name": "STUFF OFFERED BY PATREON PAGE",
            "discord_server_id": null,
            "google_analytics_id": null,
            "has_rss": false,
            "has_sent_rss_notify": false,
            "image_small_url": "[URL TO IMAGE]",
            "image_url": "[URL TO IMAGE]",
            "is_charged_immediately": false,
            "is_monthly": true,
            "is_nsfw": false,
            "main_video_embed": null,
            "main_video_url": null,
            "one_liner": null,
            "patron_count": 0,
            "pay_per_name": "month",
            "pledge_url": "/join/**************",
            "published_at": "2021-01-12T02:05:13.000+00:00",
            "rss_artwork_url": null,
            "rss_feed_title": null,
            "summary": "Creating stuff",
            "thanks_embed": null,
            "thanks_msg": null,
            "thanks_video_url": null,
            "url": "https://www.patreon.com/**************",
            "vanity": "PATREON PAGE NAME"
        },
        "id": "6026224",
        "type": "campaign"
    }, {
        "attributes": {
            "about": null,
            "created": "2017-12-01T05:39:59.000+00:00",
            "first_name": "FIRST",
            "full_name": "FIRST LAST",
            "hide_pledges": false,
            "image_url": "https://c10.patreonusercontent.com/3/XXXYYYZZZ/patreon-media/p/....jpg",
            "is_creator": false,
            "last_name": "LAST",
            "like_count": 0,
            "social_connections": {
                "deviantart": null,
                "discord": {
                    "url": null,
                    "user_id": "*************"
                },
                "facebook": null,
                "google": null,
                "instagram": null,
                "reddit": null,
                "spotify": null,
                "twitch": null,
                "twitter": null,
                "youtube": null
            },
            "thumb_url": "URL_TO_THUMBNAIL",
            "url": "https://www.patreon.com/user?u=*****",
            "vanity": null
        },
        "id": "123456666",
        "type": "user"
    }],
    "links": {
        "self": "https://www.patreon.com/api/oauth2/v2/members/****************"
    }
}

To fetch the email from the Test Payload, it required accessing an index that doesn’t even exist in the real payload structure. I, fortunately, had a very simple app and did not need a large amount of restructuring. But imagine something more robust? Why does it send test data this way?

This is likely something that needs to be updated on api side.

For such situations, i always see creating an ‘interface function’ that will return a formatted data to your app, which your app will use. The formatting can be of your own choosing, and since the function will be the only source of truth for that return, you can just adapt the function to any changes at the api while still returning the same values to your internal functions.

Is there a specific example of a function of this nature?

Anything based on the general idea of interfaces or functions which act like interfaces to rest of particular feature. Ideally a services structure in which the entire feature/code would be isolated from the rest, only interacting through a certain surface, but that’s too much for small features and codebases. So a simple wrapper function should be ok:

Imagine a function like get_patreon_member($member_id)

This function gets the api return, then reformats it. An example is here:

If you will notice, this function receives the identity return from the api, then changes/reformats the fields and hierarchy.

This was done long time ago when api and the plugin was moving to new fields and v2. By reformatting the return from the api like this, backwards compatibility for entire rest of the code including any modifications or code which was built until then was maintained. There hasnt been any hiccup among thousands of installations when these major changes in the api happened. People didnt even know anything changed and they didnt need to modify anything in their codebases.