curl --request POST \
--url https://api.markifact.com/v1/connections \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"connection_type": "gads",
"name": "Acme Agency Google Ads",
"credentials": {
"client_id": "1234567890-abc123.apps.googleusercontent.com",
"client_secret": "GOCSPX-...",
"refresh_token": "1//0g..."
},
"is_private": false
}
'import requests
url = "https://api.markifact.com/v1/connections"
payload = {
"connection_type": "gads",
"name": "Acme Agency Google Ads",
"credentials": {
"client_id": "1234567890-abc123.apps.googleusercontent.com",
"client_secret": "GOCSPX-...",
"refresh_token": "1//0g..."
},
"is_private": False
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
connection_type: 'gads',
name: 'Acme Agency Google Ads',
credentials: {
client_id: '1234567890-abc123.apps.googleusercontent.com',
client_secret: 'GOCSPX-...',
refresh_token: '1//0g...'
},
is_private: false
})
};
fetch('https://api.markifact.com/v1/connections', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.markifact.com/v1/connections",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'connection_type' => 'gads',
'name' => 'Acme Agency Google Ads',
'credentials' => [
'client_id' => '1234567890-abc123.apps.googleusercontent.com',
'client_secret' => 'GOCSPX-...',
'refresh_token' => '1//0g...'
],
'is_private' => false
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.markifact.com/v1/connections"
payload := strings.NewReader("{\n \"connection_type\": \"gads\",\n \"name\": \"Acme Agency Google Ads\",\n \"credentials\": {\n \"client_id\": \"1234567890-abc123.apps.googleusercontent.com\",\n \"client_secret\": \"GOCSPX-...\",\n \"refresh_token\": \"1//0g...\"\n },\n \"is_private\": false\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.markifact.com/v1/connections")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"connection_type\": \"gads\",\n \"name\": \"Acme Agency Google Ads\",\n \"credentials\": {\n \"client_id\": \"1234567890-abc123.apps.googleusercontent.com\",\n \"client_secret\": \"GOCSPX-...\",\n \"refresh_token\": \"1//0g...\"\n },\n \"is_private\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.markifact.com/v1/connections")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"connection_type\": \"gads\",\n \"name\": \"Acme Agency Google Ads\",\n \"credentials\": {\n \"client_id\": \"1234567890-abc123.apps.googleusercontent.com\",\n \"client_secret\": \"GOCSPX-...\",\n \"refresh_token\": \"1//0g...\"\n },\n \"is_private\": false\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"type": "<string>",
"is_private": true,
"display_name": "<string>",
"external_id": "<string>",
"created_at": 123,
"updated_at": 123
}{
"detail": "<unknown>"
}{
"detail": "<unknown>"
}{
"detail": "<unknown>"
}Create connection
Create a connection from credentials you already hold, verified with the provider before anything is stored.
curl --request POST \
--url https://api.markifact.com/v1/connections \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"connection_type": "gads",
"name": "Acme Agency Google Ads",
"credentials": {
"client_id": "1234567890-abc123.apps.googleusercontent.com",
"client_secret": "GOCSPX-...",
"refresh_token": "1//0g..."
},
"is_private": false
}
'import requests
url = "https://api.markifact.com/v1/connections"
payload = {
"connection_type": "gads",
"name": "Acme Agency Google Ads",
"credentials": {
"client_id": "1234567890-abc123.apps.googleusercontent.com",
"client_secret": "GOCSPX-...",
"refresh_token": "1//0g..."
},
"is_private": False
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
connection_type: 'gads',
name: 'Acme Agency Google Ads',
credentials: {
client_id: '1234567890-abc123.apps.googleusercontent.com',
client_secret: 'GOCSPX-...',
refresh_token: '1//0g...'
},
is_private: false
})
};
fetch('https://api.markifact.com/v1/connections', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.markifact.com/v1/connections",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'connection_type' => 'gads',
'name' => 'Acme Agency Google Ads',
'credentials' => [
'client_id' => '1234567890-abc123.apps.googleusercontent.com',
'client_secret' => 'GOCSPX-...',
'refresh_token' => '1//0g...'
],
'is_private' => false
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.markifact.com/v1/connections"
payload := strings.NewReader("{\n \"connection_type\": \"gads\",\n \"name\": \"Acme Agency Google Ads\",\n \"credentials\": {\n \"client_id\": \"1234567890-abc123.apps.googleusercontent.com\",\n \"client_secret\": \"GOCSPX-...\",\n \"refresh_token\": \"1//0g...\"\n },\n \"is_private\": false\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.markifact.com/v1/connections")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"connection_type\": \"gads\",\n \"name\": \"Acme Agency Google Ads\",\n \"credentials\": {\n \"client_id\": \"1234567890-abc123.apps.googleusercontent.com\",\n \"client_secret\": \"GOCSPX-...\",\n \"refresh_token\": \"1//0g...\"\n },\n \"is_private\": false\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.markifact.com/v1/connections")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"connection_type\": \"gads\",\n \"name\": \"Acme Agency Google Ads\",\n \"credentials\": {\n \"client_id\": \"1234567890-abc123.apps.googleusercontent.com\",\n \"client_secret\": \"GOCSPX-...\",\n \"refresh_token\": \"1//0g...\"\n },\n \"is_private\": false\n}"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"type": "<string>",
"is_private": true,
"display_name": "<string>",
"external_id": "<string>",
"created_at": 123,
"updated_at": 123
}{
"detail": "<unknown>"
}{
"detail": "<unknown>"
}{
"detail": "<unknown>"
}Endpoint
POST /v1/connections
Which types this works for
Call List connection types and look atauth_methods:
auth_methods | What it means |
|---|---|
["manual"] | Only this endpoint. There is no Markifact OAuth app for the channel, so credentials are the only way in. Shopify, Ahrefs, and OpenAI Ads work this way. |
["oauth", "manual"] | Either way. Using your own app here is a white-label connection, so your clients never see Markifact in their account. |
["oauth"] | Not this endpoint. Use Create auth link. |
credential_fields, which is exactly what belongs in credentials, and a docs_url pointing at a plain-markdown page that explains where each of those values comes from. Fetch that page when you need the steps for a channel.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
connection_type | string | Yes | A type whose auth_methods include manual, such as gads or shopify. |
name | string | Yes | Your label for the connection, up to 100 characters. Shown wherever connections are listed. |
credentials | object | Yes | Exactly the credential_fields the type lists. A missing or unrecognised field is rejected before any call to the provider. |
is_private | boolean | No | Whether the connection should be private to the API-key user. Defaults to false, meaning shared with the workspace so other team members can see and use it. |
Example Request
curl https://api.markifact.com/v1/connections \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"connection_type": "gads",
"name": "Acme Agency Google Ads",
"credentials": {
"client_id": "1234567890-abc123.apps.googleusercontent.com",
"client_secret": "GOCSPX-...",
"refresh_token": "1//0g..."
},
"is_private": false
}'
Response
{
"id": "1f2e3d4c-0000-0000-0000-000000000000",
"type": "gads",
"display_name": "Acme Agency Google Ads",
"external_id": null,
"is_private": false,
"created_at": 1764086400,
"updated_at": 1764086400
}
Response Fields
| Field | Type | Description |
|---|---|---|
id | string | Markifact connection ID. Pass this as connection_id when running operations. |
type | string | The connection type you created. |
display_name | string | The name you sent. |
external_id | string, null | Always null here. External IDs are set when creating an auth link. |
is_private | boolean | Whether the connection is restricted to the API-key user. |
created_at | integer | Creation time as a Unix timestamp in seconds. |
updated_at | integer | Last update time as a Unix timestamp in seconds. |
Rotating credentials
Which connection a request updates depends on how the type derives its ID:| Types | Keyed on | Posting the same values again |
|---|---|---|
| White-label channels, such as Google Ads, Meta Ads or TikTok Ads | Type and name | Replaces the credentials in place. Same connection ID, so bound workflows keep running. A different name creates a separate connection. |
| Slack | Workspace and app | Replaces in place whatever name you send. |
| Shopify | Store | Replaces in place whatever name you send. |
| Ahrefs, SEMrush, Adjust, DataForSEO, OpenAI Ads | Nothing, each post is new | Creates a new connection. Rebind your workflows to it, then delete the old one. |
is_private out when replacing credentials. Omitting it keeps the connection’s current visibility.
Errors
| Status | When |
|---|---|
400 | The type cannot be created from credentials, a credential field is missing or unrecognised, or the provider rejected the credentials. |
401 | Invalid API key. |
403 | The connection type is white-label capable and your team is not on the Team plan. |
detail, so you can tell an expired refresh token from an app that lacks the right scope.
{
"detail": "Google rejected the refresh token (invalid_grant). Make sure it was minted with this exact client ID, has not been revoked, and that the OAuth consent screen is published (apps left in Testing status issue refresh tokens that expire after 7 days)."
}
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
A connection type whose auth_methods include "manual". See GET /v1/connections/types.
"gads"
Your label for the connection. For white-label types, posting the same type and name again replaces the stored credentials in place. Slack and Shopify are keyed on the workspace and store instead, and API-key types (Ahrefs, SEMrush, Adjust, DataForSEO, OpenAI Ads) always create a new connection.
100"Acme Agency Google Ads"
Exactly the credential_fields the type lists. A missing or unrecognised field is rejected before any call to the provider.
Show child attributes
Show child attributes
{
"client_id": "1234567890-abc123.apps.googleusercontent.com",
"client_secret": "GOCSPX-...",
"refresh_token": "1//0g..."
}
Whether the connection should be private to the API-key user. Defaults to false, meaning shared with the workspace so other team members can see and use it. Omit it when replacing credentials on an existing connection and the current visibility is kept.

