Skip to main content

API Documentation

Updated yesterday

Overview

Adpulse is backed by a GraphQL API that supports all the data and functionality available in app.

This means that you need to familiarise yourself with how GraphQL apis are used. Apart from the official documentation linked above, Github also hosts a good guide on how to use this query language that may be handy for someone coming from REST api background. You can find this and other useful resources here

You can also find a more in depth documentation in the GraphQL web sites here and here.

Tooling

GraphQL has a wide range of tools to aid developers implement API clients or simply explore APIs, you can browse them here. Additionally most IDEs would have tools to browse GraphQL API schemas and generate code from them.

Some developer tools that Adpulse team can recommend are:

Should you have any questions, please email [email protected]


Getting Access

API Schema

The API schema can be found at https://eu.api.adpulse.app/graphql/schema

curl https://eu.api.adpulse.app/graphql/schema -o adpulse.graphqls

Access Token

To access the api (beyond its schema), you need to include an access token in the HTTP authorization header:

export ADPULSE_HOME=https://eu.api.adpulse.app
export ADPULSE_TOKEN='<my-token>'
curl -H "Authorization: Bearer ${ADPULSE_TOKEN}" -X POST ${ADPULSE_HOME}/graphql --data @my-query.json

Your account owner needs to generate a "Developer Token" to access the API by browsing to the "Developer" Tab in the Admin section of the Settings menu in app. Make sure you store this token in a secure place, as this token won't be visible anymore after generation.


Using the API

Running Your First Query

GraphQL APIs have two types of requests: queries and mutations. Queries are read only and are equivalent to GET requests in REST APIs whereas mutations update data in the same way a POST, PUT or DELETE would do in a REST api.

All queries and mutations are sent via a HTTP POST request that must include a JSON body specifying the query and any variables needed for the server to fulfill your request.

Strong Types

One of the advantages of using GraphQL is that we must define and use strongly and constrained types to represent the data used across our app. This allows ensuring that a field that is marked as "required" will never be absent from an entity or that a type marked as a number will never fail to be numeric and therefore a developer can be sure to use them safely in their code.

For example, if you find the AdAccountInterface type defined in the schema you will notice that fields like id, name and platform are required and will always be provided:

interface AdAccountInterface {
platform: AdPlatform!
id: String!
inserted: Instant!
name: String!
timeZone: TimeZone!
currencyCode: CurrencyCode!
performanceDataAvailableFrom: LocalDate
flags: AdAccountFlags!
status: AdAccountStatus!
scans: [ScanReferenced!]!
insights: AdAccountInsights!
tags: [Tag!]!
}

Writing Your Query

To query for ad account id, name and platform, you would write a query like this:

query fetchAdAccounts {
adAccounts(limit: 10, offset: 0){
nodes {
__typename
... on AdAccountInterface {
id
name
platform
}
}
}
}

Note how we are only requesting three fields from the AdAccountInterface even though it has many more. This is another advantage that GraphQL offers: to request only the data needed to fulfill a requirement. Adpulse recommends that when running your queries, you only request the data that you plan to use.

Another important thing to note is that querying data from the Adpulse API offers several facilities to filter, sort, and paginate through your data sets, which is something that you will come across in most queries.

Run Your Query

In practice, you would use your programming language facilities to execute the query above; however, in this instance, we can use curl for testing purposes:

curl -X POST ${ADPULSE_HOME}/graphql \
-H "Authorization: Bearer ${ADPULSE_TOKEN}" \
-H "Content-Type: application/json" \
--data @- << EOF
{
"query": "query fetchAdAccounts {
adAccounts(limit: 10, offset: 0){
nodes {
__typename
... on AdAccountInterface {
id
name
platform
}
}
}
}"
}
EOF

This would produce something like:

{
"data": {
"adAccounts": {
"nodes": [
{
"__typename": "AdAccountGoogle",
"id": "1234567890",
"name": "Mos Eisley Cantina",
"platform": "google"
},
{
"__typename": "AdAccountMicrosoft",
"id": "789064321",
"name": "Hunter's Guild Nevarro",
"platform": "microsoft"
}
]
}
}
}

In those results, we can see the __typename varies depending on the platform to which an ad account belongs (which depends on the ad platforms you have connected and the accounts you have imported into Adpulse).

Go ahead and experiment querying specific fields for the more specific types. Hint use the ... on TypeName syntax to access them, ie ... on AdAccountGoogle or ... on AdAccountMicrosoft


Pagination, Sorting, and Filtering

For this section, we will explain how to use Adpulse GraphQL query facilities with the budgets data as a use case to answer the questions:

  1. What is the best-performing budget in terms of pacing?

  2. What is the worst?

Note: To keep this section simple and constrained, the GraphQL types mentioned are reduced to simple versions, removing details but keeping it accurate for the selected use case.

The Budget type is the result of a union of types, and it looks like this:

union Budget__new = BudgetCustomV2 | BudgetGoogleAccount

And some of the fields for the BudgetCustomV2 look like:

type BudgetCustomV2 {
currencyCode: CurrencyCode!
entries: BudgetEntries!
id: ID!
inserted: Instant!
name: String!
spend: BudgetCustomV2Spend!
status: BudgetStatus!
campaignBudgetGroup: CampaignBudgetGroup!
}

From there, we can inspect the entries item and access its current entry field that looks like the following:

type BudgetEntry {
clicks: Long!
conversions: Float!
id: ID!
impressions: Long!
lastUpdated: Instant!
pacing: BigDecimal
progress: BigDecimal
spend: BigDecimal!
}

The field we are interested in here is the pacing field. See how pacing works here.

Connections and Pagination

In Adpulse, most datasets can be paginated through by using offset pagination, and a few others using cursor-based pagination. In general, the offset pagination data type looks like:

type ConnectionMyType {
nodes: [MyType!]!
offsetInfo: OffsetInfo!
total: Long
}

Where OffsetInfo looks like:

type OffsetInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
}

Connection queries will always include a limit: Int and an offset: Int parameters that tell the server what records to return. Sensible defaults are in place, generally documented on each connection query.

For our use case, the connection that we need to query is the BudgetTreeConnection, this connection, which holds nodes of type BudgetTree which is another union of several budget types, the details for this guide are not too important; however, we encourage you to inspect the possible types and get an intuition of what they represent. For demonstration purposes, the budget tree type that we will use here is the BudgetTreeSingleRoot which represents a root budget that has at least one child budget in it.

The query that gives us access to this connection looks like (some details omitted):

budgetTree(
"Maximum number of values is 1,000"
adAccountIds: [AdAccountIdInput!],
"Filter expression. Supported variables are 'name: String!'"
filter: String,
"Maximum allowed value is 100"
limit: Int,
"Maximum allowed value is 5,000"
offset: Int,
"Order the results by providing pairs of columns and sort directions"
order: [BudgetTreeConnectionOrder!],
platforms: [AdPlatform!],
): BudgetTreeConnection

A query fragment to get the pacing field from the root budget within the BudgetTreeSingleRoot would look like:

fragment budgetTreeSingleRoot on BudgetTreeSingleRoot {
__typename
budget {
... on BudgetCustomV2 {
id
name
entries {
current {
pacing
}
}
campaignBudgetGroup {
adAccounts {
... on AdAccountInterface {
id
name
}
}
}
}
}
}

Take note of how the query definition includes several parameters relevant to the dataset in question. In Adpulse, all offset queries would include limit, offset and order, whereas the filter field would be in most of them.

Sorting

Now that we know the connection we need to use and the query that gives us access to it, we can proceed to write and execute a query to answer our questions.

Our full query would look like:

fragment budgetTreeSingleRoot on BudgetTreeSingleRoot {
__typename
budget {
... on BudgetCustomV2 {
id
name
entries {
current {
pacing
}
}
campaignBudgetGroup {
adAccounts {
... on AdAccountInterface {
id
name
}
}
}
}
}
}

query fetchBestBudgets(
$order: [BudgetTreeConnectionOrder!]
) {
budgetTree(limit: 5, order: $order, offset: 0, filter: null){
nodes {
__typename
... on BudgetTreeSingleRoot{
...budgetTreeSingleRoot
}
}
}
}

This queries for the top 5 budgets ordered according to what's specified by the $order parameter. We'll use the pacing field in this case.

Now, to be able to run this query using curl we need to json encode the query above and pass it to curl in an appropriate format, a query.json file with the full query would look like:

{
"variables": {
"order": {
"column": "pacing",
"direction": "desc"
}
},
"query": "fragment budgetTreeSingleRoot on BudgetTreeSingleRoot {\n __typename\n budget {\n ... on BudgetCustomV2 {\n id\n name\n entries {\n current {\n target\n spend\n pacing\n }\n }\n campaignBudgetGroup {\n adAccounts {\n ... on AdAccountInterface {\n id\n name\n }\n }\n }\n }\n }\n }\n \n query fetchBestBudgets(\n $order: [BudgetTreeConnectionOrder!]\n ) {\n budgetTree(limit: 5, order: $order, offset: 0, filter: null){\n nodes {\n __typename\n ... on BudgetTreeSingleRoot{\n ...budgetTreeSingleRoot\n }\n }\n }\n }"
}

Then we can simply execute:

curl -X POST ${ADPULSE_HOME}/graphql \
-H "Authorization: Bearer ${ADPULSE_TOKEN}" \
-H "Content-Type: application/json" \
--data @query.json

This could return results similar to the following:

{
"data": {
"budgetTree": {
"nodes": [
{
"__typename": "BudgetTreeSingleRoot",
"budget": {
"id": "bdg_2y4jc6mlbvaalncohugziljdjm",
"name": "Mos Eisley Cantina - All Campaigns",
"entries": {
"current": {
"target": 220,
"spend": 55.35,
"pacing": 108.05438277104233
}
},
"campaignBudgetGroup": {
"adAccounts": [
{
"id": "1234567890",
"name": "Mos Eisley Cantina"
}
]
}
}
},
{
"__typename": "BudgetTreeSingleRoot",
"budget": {
"id": "bdg_cs7eyhkxu5fyzl73e2vi6tpuum",
"name": "Hunter's Guild Nevarro - All Campaigns",
"entries": {
"current": {
"target": 1000,
"spend": 74.793311,
"pacing": 49.92824999205298
}
},
"campaignBudgetGroup": {
"adAccounts": [
{
"id": "789064321",
"name": "Hunter's Guild Nevarro"
}
]
}
}
},
{
"__typename": "BudgetTreeClientAllSelectedBudget"
}
]
}
}
}

In this instance, the account has only three budgets, two of BudgetTreeSingleRoot type and one of a legacy type BudgetTreeClientAllSelectedBudget that we have chosen to ignore for this exercise.

Now, to answer the questions in this example is simple, given that we have only two budgets, one pacing at 108% and another one at 49.93%. And according to the definition of on-target pacing, the budget "Mos Eisley Cantina - All Campaigns" is the best-performing budget, and "Hunter's Guild Nevarro" is not doing too well.

Filtering

Whenever possible, Adpulse recommends filtering your data down to only the records you plan to use. To do that, most queries provide parameters to filter down as much as possible the records returned. Filter by ad platform, by ad account ids, and by tags are some of the most common filters provided.

In our use case, imagine we had hundreds of budgets, we would need to limit the data fetched somehow. For example, we could just query for budgets with google ad accounts and perhaps containing "Mos Eisley" in their budget name. Tags are also a very good option for grouping and selecting records.

To filter down our request using platform and budget name we can simply define the variables in our query:

query fetchBestBudgets(
$order: [BudgetTreeConnectionOrder!],
$filter: String!,
$plaforms: [AdPlatform!]
) {
budgetTree(limit: 5, order: $order, offset: 0, filter: $filter, platforms: $plaforms){
nodes {
__typename
... on BudgetTreeSingleRoot{
...budgetTreeSingleRoot
}
}
}
}

Define a filter variable as follows:

{
"variables": {
"order": {
"column": "pacing",
"direction": "desc"
},
"filter": "contains(\"name\", \"Mos Eisley\")",
"platforms": ["google"]
},
"query": "fragment budgetTreeSingleRoot on BudgetTreeSingleRoot {\n __typename\n budget {\n ... on BudgetCustomV2 {\n id\n name\n entries {\n current {\n target\n spend\n pacing\n }\n }\n campaignBudgetGroup {\n kpi {\n __typename\n }\n adAccounts {\n ... on AdAccountInterface {\n id\n name\n }\n }\n }\n }\n }\n}\n\nquery fetchBestBudgets(\n $order: [BudgetTreeConnectionOrder!],\n $filter: String!,\n $plaforms: [AdPlatform!]\n) {\n budgetTree(limit: 5, order: $order, offset: 0, filter: $filter, platforms: $plaforms){\n nodes {\n __typename\n ... on BudgetTreeSingleRoot{\n ...budgetTreeSingleRoot\n }\n }\n }\n}"
}

This would narrow down the results to just one record from our sample dataset.

Tags Filtering

Tags are a very useful feature that allows filtering your data. The tag filter type looks like:

tagIds: [[ID!]!]

This means the tagIds take a bidimensional array of tag ids. Its functionality is as follows:

  • The outer array elements are joined in an OR operation. For example, if you want to fetch records that have the tag id 1, 2, or 3, you would set the tagIds parameter to [[1], [2], [3]]

  • The inner array elements are joined with and AND operation, so if you want to fetch records that have the tag id 1 and 2, you would set the parameter to [[1, 2]].

  • An empty array tagIds = [], means return records that do not have any tags attached to them.

There is also a notTagIds parameter that allows you to exclude records by tags. The functionality is basically the negation of tagIds.

Cursor-Based Pagination

Cursor-based pagination is a well-known standard when using GraphQL, in the case of Adpulse, there are a handful of queries that work using this mechanism. NoteConnection and ChangeLogConnection are good examples.

The types in Adpulse look like the following:

type ChangeLogConnection {
edges: [ChangeLogConnectionEdge!]!
pageInfo: PageInfo!
}

type ChangeLogConnectionEdge {
cursor: String!
node: ChangeLog!
}

type PageInfo {
endCursor: String!
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String!
}

The ChangeLog type is made up of a large union describing each change that may have occurred due to actions performed by your users. We are not going to go into the details, but go ahead and review the type for your own understanding.

To query for change logs, we need to use the changeLog query that looks somewhat like the following:

changeLog(
"Maximum allowed value is 1,000"
adAccountIds: [AdAccountIdInput!],
after: String,
before: String,
"Maximum allowed value is 1,000"
budgetIds: [ID!],
"Filter expression. Supported variables are 'timestamp: Timestamp!', 'user.name: String!', 'user.email: String!'"
filter: String,
"Maximum allowed value is 500"
first: Int,
"Maximum allowed value is 500"
last: Int,
platforms: [AdPlatform!],
"To limit ad accounts with either tag id 1, 2, or 3: [[1], [2], [3]]. To limit ad accounts that have tag id 1 and 2, or 3: [[1, 2], 3]. To limit ad accounts with no tag: []."
tagIds: [[ID!]!],
types: [ChangeLogType!]
): ChangeLogConnection

A query to paginate from the most recent changes would look like:

fragment changeLogConnection on ChangeLogConnection {
edges {
cursor
node {
id
user {
name
}
entry {
__typename
}
}
}
pageInfo {
startCursor
endCursor
hasNextPage
hasPreviousPage
}
}

query fetchChangeLogs {
changeLog {
...changeLogConnection
}
}

Variables:

{
"first": 25
}

Running this query will return the newest 25 change log records. The edge field holds the node with the change log data, along with a cursor (pointer) to itself. This pointer is special because it identifies the node across the dataset and can be used as the after or before parameter.

A possible response payload could look like the following (only the last edge showed):

{
"data": {
"changeLog": {
"pageInfo": {
"hasPreviousPage": false,
"hasNextPage": true,
"startCursor": "aWQ6MTAwMDk4Mw==",
"endCursor": "aWQ6MTAwMDk1OQ=="
},
"edges": [
{
"cursor": "aWQ6MTAwMDk1OQ==",
"node": {
"id": "1000959",
"user": {
"name": "Greef Karga"
},
"entry": {
"__typename": "ChangeLogEntryAdAccountLinked"
}
}
}
]
}
}
}

In our subsequent request to get the next "page" we need to use the endCursor as our after parameter so that Adpulse API correctly returns the subsequent page:

{
"first": 25,
"after": "aWQ6MTAwMDk1OQ=="
}

The next page could look like the following (only the first and last edge showed):

{
"data": {
"changeLog": {
"pageInfo": {
"hasPreviousPage": true,
"hasNextPage": true,
"startCursor": "aWQ6MTAwMDk1OA==",
"endCursor": "aWQ6MTAwMDkzNA=="
},
"edges": [
{
"cursor": "aWQ6MTAwMDk1OA==",
"node": {
"id": "1000958",
"user": {
"name": "Han"
},
"entry": {
"__typename": "ChangeLogEntryUserInvitationCreated"
}
}
},
{
"cursor": "aWQ6MTAwMDkzNA==",
"node": {
"id": "1000934",
"user": {
"name": "Fett"
},
"entry": {
"__typename": "ChangeLogEntryTagCreated"
}
}
}
]
}
}
}

Lastly, if we wanted to fetch the previous page (page 2) of data, then we would use the startCursor as our before parameter as follows, but we need to swap to ask for the last 25 items and not the first (we need the last 25, because there are at least 50 records before page 3):

{
"last": 25,
"before": "aWQ6MTAwMDk1OA=="
}


Mutations

Updates can be performed using GraphQL mutations. They work in the same way as queries but their intention is to update data at the server side and they may not return data.

A simple mutation to update your preferred name in Adpulse would look like the following:

mutation updateMyPreferredName($preferredName: String) {
userCurrentUpdate(preferredName: $preferredName){
id
preferredName
}
}

Executing this query would look a lot like the queries we've seen before:

{
"variables": {
"preferredName": "Din"
},
"query":"mutation updateMyPreferredName($preferredName: String) {\n userCurrentUpdate(preferredName: $preferredName){\n id\n preferredName\n }\n}"
}
curl -X POST ${ADPULSE_HOME}/graphql \
-H "Authorization: Bearer ${ADPULSE_TOKEN}" \
-H "Content-Type: application/json" \
--data @query.json

In this instance the userCurrentUpdate mutation returns the current user data as part of its response, so we would see a response similar to:

{
"data": {
"userCurrentUpdate": {
"id": "usr_a6zze6zwizghhk5cl2z5aoin3m",
"preferredName": "Din"
}
}
}


Schema Changes

Adpulse is committed to bringing the best features in the app to our customers. For the Adpulse API this means the schema evolves rapidly. New features are added constantly, and existing features are updated from time to time. You should be aware that a query or type that you rely on may change without notice. An effort is made to mark deprecated items as such and keep the schema backwards compatible for a period of time. However, there is a chance that breaking changes may be needed, so you should keep up to date with any schema changes that may accompany new or updated features in app.

A good trigger for you to check for any api updates is whenever a new release is announced on Adpulse release notes. We recommend you update the schema after every release or more often as you see fit.


Query Restrictions

Nested Queries

Please note that, given the nature of GraphQL, some queries may lead to deeply nested fields being requested, for example, a tag may be assigned to a user, and all users have a tag:

type User {
id: ID!
lastLogin: Instant
name: String!
preferredName: String
tag: Tag
}

type Tag {
color: String!
"1-255 characters"
description: String
id: ID!
isUserTag: Boolean!
"1-80 characters"
name: String!
user: User
}

A query for tags may request the user data that could be related to it:

query fetchTags {
tags{
nodes {
tag {
id
name
isUserTag
user {
id
name
}
}
}
}
}

This will be allowed, and no errors will be reported. Howeve,r nesting a request for the user tag will be blocked:

query fetchTags {
tags{
nodes {
tag {
id
name
isUserTag
user {
id
name
tag {
id
name
}
}
}
}
}
}

This query will be rejected with an error similar to:

{
"data": null,
"errors": [
{
"message": "Invalid path requested: already within tag",
"locations": [
{
"line": 54,
"column": 11
}
],
"extensions": {
"status": 400,
"error_id": null,
"code": "graphql_validation_error"
}
}
]
}

As a rule of thumb, a query that goes too deep and/or requests data that may be available at a previous level will fail validation.

Did this answer your question?