Skip to content
Syncline

Gmail API reference

<p>These are the Syncline endpoints for the Gmail connector. They are served by the Syncline API, authenticated with a Syncline key, and shaped like every other Syncline response. This page is not documentation of Google's Gmail API, and none of these paths exist on Google's side: for the vendor routes, read Google's own developer documentation.</p><p>The connector holds the OAuth grant for one mailbox, so your code never sees a Google token. You supply a Gmail search string and we run it on Google's side, which means messages that do not match are never returned to Syncline at all. Sends go out from the connected address with your signature and no branding from us.</p>
Base URL: https://api.syncline.topclb.com

Authentication

Send a Syncline secret key as a bearer token: Authorization: Bearer sk_live_... . The connection_id parameter selects which authorised mailbox to use. Read and send are separate scopes on the underlying grant, so a connection created for reading cannot be used to send until you reauthorise it and say so explicitly.

Endpoints

<article class="sy-endpoint"><h3><code>GET /v1/connectors/gmail/messages</code></h3><p>Search the mailbox with a Gmail query string and return matching messages.</p><div class="sy-table-wrap"><table><thead><tr><th>Param</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>connection_id</td><td>string</td><td>yes</td><td>The authorised mailbox to search.</td></tr><tr><td>q</td><td>string</td><td>yes</td><td>A Gmail search string, for example label:support -label:synced. Runs on Google's side.</td></tr><tr><td>include_body</td><td>boolean</td><td>no</td><td>Return the message body as well as the headers. Defaults to false to keep responses small.</td></tr><tr><td>limit</td><td>integer</td><td>no</td><td>Results per page, 1 to 100. Defaults to 25.</td></tr><tr><td>cursor</td><td>string</td><td>no</td><td>Opaque cursor from the previous response.</td></tr></tbody></table></div><pre><code>{ &quot;run_id&quot;: &quot;run_6f38db04&quot;, &quot;connector&quot;: &quot;gmail&quot;, &quot;status&quot;: &quot;ok&quot;, &quot;data&quot;: [ { &quot;message_id&quot;: &quot;18f2a9c7b1e04d55&quot;, &quot;thread_id&quot;: &quot;18f2a9c7b1e04d55&quot;, &quot;from&quot;: &quot;lena@harbourline.example&quot;, &quot;subject&quot;: &quot;Invoice 20481 looks wrong&quot;, &quot;received_at&quot;: &quot;2026-09-07T07:31:09Z&quot;, &quot;has_attachments&quot;: true, &quot;labels&quot;: [&quot;INBOX&quot;, &quot;support&quot;] } ], &quot;next_cursor&quot;: null }</code></pre></article><article class="sy-endpoint"><h3><code>POST /v1/connectors/gmail/messages</code></h3><p>Send an email from the connected address, optionally inside an existing thread.</p><div class="sy-table-wrap"><table><thead><tr><th>Param</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>connection_id</td><td>string</td><td>yes</td><td>The authorised mailbox to send from.</td></tr><tr><td>to</td><td>array</td><td>yes</td><td>One or more recipient addresses. Each is validated before anything is sent.</td></tr><tr><td>subject</td><td>string</td><td>yes</td><td>The subject line. Ignored when thread_id is supplied, because a reply keeps the original.</td></tr><tr><td>body_html</td><td>string</td><td>yes</td><td>The message body. A plain text alternative is generated automatically.</td></tr><tr><td>thread_id</td><td>string</td><td>no</td><td>Reply inside this thread rather than starting a new conversation.</td></tr></tbody></table></div><pre><code>{ &quot;run_id&quot;: &quot;run_70491ec5&quot;, &quot;connector&quot;: &quot;gmail&quot;, &quot;status&quot;: &quot;ok&quot;, &quot;data&quot;: { &quot;message_id&quot;: &quot;18f2b04e9c117a30&quot;, &quot;thread_id&quot;: &quot;18f2a9c7b1e04d55&quot;, &quot;to&quot;: [&quot;lena@harbourline.example&quot;], &quot;sent_at&quot;: &quot;2026-09-07T10:44:18Z&quot; } }</code></pre></article><article class="sy-endpoint"><h3><code>POST /v1/connectors/gmail/messages/{message_id}/labels</code></h3><p>Apply or remove labels, the usual way to stop a message being processed twice.</p><div class="sy-table-wrap"><table><thead><tr><th>Param</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>connection_id</td><td>string</td><td>yes</td><td>The authorised mailbox holding the message.</td></tr><tr><td>add</td><td>array</td><td>no</td><td>Label names to apply. A label that does not exist is created when create_missing is true.</td></tr><tr><td>remove</td><td>array</td><td>no</td><td>Label names to strip from the message.</td></tr><tr><td>create_missing</td><td>boolean</td><td>no</td><td>Create any label in add that the mailbox does not have yet. Defaults to false.</td></tr></tbody></table></div><pre><code>{ &quot;run_id&quot;: &quot;run_8153fd16&quot;, &quot;connector&quot;: &quot;gmail&quot;, &quot;status&quot;: &quot;ok&quot;, &quot;data&quot;: { &quot;message_id&quot;: &quot;18f2a9c7b1e04d55&quot;, &quot;labels&quot;: [&quot;INBOX&quot;, &quot;support&quot;, &quot;synced&quot;], &quot;created&quot;: [&quot;synced&quot;] } }</code></pre></article><article class="sy-endpoint"><h3><code>GET /v1/connectors/gmail/attachments/{attachment_id}</code></h3><p>Fetch an attachment, or a short lived link when the file is too large to inline.</p><div class="sy-table-wrap"><table><thead><tr><th>Param</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>connection_id</td><td>string</td><td>yes</td><td>The authorised mailbox holding the attachment.</td></tr><tr><td>message_id</td><td>string</td><td>yes</td><td>The message the attachment belongs to.</td></tr><tr><td>as</td><td>string</td><td>no</td><td>One of link or base64. Defaults to link, which is what most destination apps prefer.</td></tr></tbody></table></div><pre><code>{ &quot;run_id&quot;: &quot;run_92640e27&quot;, &quot;connector&quot;: &quot;gmail&quot;, &quot;status&quot;: &quot;ok&quot;, &quot;data&quot;: { &quot;attachment_id&quot;: &quot;att_c41a7f&quot;, &quot;filename&quot;: &quot;invoice-20481.pdf&quot;, &quot;mime_type&quot;: &quot;application/pdf&quot;, &quot;size_bytes&quot;: 184320, &quot;download_url&quot;: &quot;https://files.syncline.topclb.com/t/9f31c0aa&quot;, &quot;expires_at&quot;: &quot;2026-09-07T12:44:18Z&quot; } }</code></pre></article><article class="sy-endpoint"><h3><code>POST /v1/connectors/gmail/drafts</code></h3><p>Prepare a message and leave it unsent, for flows where a person reads it first.</p><div class="sy-table-wrap"><table><thead><tr><th>Param</th><th>Type</th><th>Required</th><th>Description</th></tr></thead><tbody><tr><td>connection_id</td><td>string</td><td>yes</td><td>The authorised mailbox to create the draft in.</td></tr><tr><td>to</td><td>array</td><td>yes</td><td>One or more recipient addresses.</td></tr><tr><td>subject</td><td>string</td><td>yes</td><td>The subject line for the draft.</td></tr><tr><td>body_html</td><td>string</td><td>yes</td><td>The draft body. Nothing is sent until a person clicks send in Gmail.</td></tr></tbody></table></div><pre><code>{ &quot;run_id&quot;: &quot;run_a3751f38&quot;, &quot;connector&quot;: &quot;gmail&quot;, &quot;status&quot;: &quot;ok&quot;, &quot;data&quot;: { &quot;draft_id&quot;: &quot;r-8841002937&quot;, &quot;message_id&quot;: &quot;18f2b1aa30f5c204&quot;, &quot;created_at&quot;: &quot;2026-09-07T10:51:40Z&quot; } }</code></pre></article>

Errors

<div class="sy-table-wrap"><table><thead><tr><th>Code</th><th>Meaning</th><th>What to do</th></tr></thead><tbody><tr><td>401 connection_revoked</td><td>The Google grant was removed, often from the account security page or by a workspace admin policy.</td><td>Reconnect the mailbox from Connections. Nothing in the queue is lost while the connection is down.</td></tr><tr><td>403 send_scope_missing</td><td>This connection was authorised for reading only and the call tried to send or draft.</td><td>Reauthorise the mailbox and tick send during the consent step, then retry the run.</td></tr><tr><td>404 message_not_found</td><td>The message was deleted, or it lives in a mailbox other than the one this connection holds.</td><td>Check the message ID against a fresh search. A deleted message cannot be recovered through the API.</td></tr><tr><td>413 attachment_too_large</td><td>The file exceeds the 25 megabyte inline limit, so the bytes cannot be passed through in the response.</td><td>Request the attachment with as=link. Most destination apps accept a download link and store it happily.</td></tr><tr><td>429 upstream_rate_limited</td><td>Google's per user quota was reached, usually during a large backfill rather than normal running.</td><td>Lower the backfill batch size, or wait for the retry_after window. Normal flows almost never reach this.</td></tr></tbody></table></div>

Rate limits

Syncline allows 120 requests per minute per API key. Google's own budget is generous for a single mailbox, so the practical ceiling here is ours rather than theirs, except during a backfill. Backfill runs should be issued with a batch size of 50 or lower, which keeps a 10000 message import inside both budgets and takes roughly 40 minutes.

Code samples

<h4>curl</h4><pre><code>curl -G https://api.syncline.topclb.com/v1/connectors/gmail/messages \ -H &quot;Authorization: Bearer $SYNCLINE_KEY&quot; \ --data-urlencode &quot;connection_id=con_gmail_2v9&quot; \ --data-urlencode &quot;q=label:support -label:synced&quot; \ --data-urlencode &quot;limit=25&quot;</code></pre><h4>node</h4><pre><code>const url = new URL( &quot;https://api.syncline.topclb.com/v1/connectors/gmail/messages&quot; ); url.searchParams.set(&quot;connection_id&quot;, &quot;con_gmail_2v9&quot;); url.searchParams.set(&quot;q&quot;, &quot;label:support -label:synced&quot;); const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.SYNCLINE_KEY}` } }); const { data } = await res.json(); for (const msg of data) console.log(msg.subject);</code></pre><h4>python</h4><pre><code>import os import requests resp = requests.get( &quot;https://api.syncline.topclb.com/v1/connectors/gmail/messages&quot;, headers={&quot;Authorization&quot;: f&quot;Bearer {os.environ['SYNCLINE_KEY']}&quot;}, params={ &quot;connection_id&quot;: &quot;con_gmail_2v9&quot;, &quot;q&quot;: &quot;label:support -label:synced&quot;, &quot;limit&quot;: 25, }, timeout=30, ) resp.raise_for_status() for message in resp.json()[&quot;data&quot;]: print(message[&quot;subject&quot;])</code></pre>