Automate Gmail lead capture using users.messages.list with Gmail search syntax to filter lead emails, then users.messages.get to parse body content and extract contact information. Key gotcha: messages.list returns only {id, threadId} — each message content requires a separate messages.get call costing 5 quota units. For real-time capture, Gmail Push via Cloud Pub/Sub is needed. The gmail.readonly scope is Restricted and requires OAuth verification.
API Quick Reference
OAuth 2.0
15,000 quota units/minute; 5 units per message get
JSON
Available
Understanding the Gmail API
The Gmail API provides read access to inbox messages through a two-step process: first list message IDs using users.messages.list with Gmail search syntax, then fetch each message's full content using users.messages.get. This two-step design means processing 100 leads requires at minimum 101 quota units (1 list + 100 gets at 5 units each = 501 units).
For lead capture automation, the search query (the 'q' parameter) is your key tool. Gmail search syntax is identical to what you type in the Gmail search bar — you can match on from:, subject:, after:, before:, has:attachment, and boolean operators. Use after: with a timestamp to only fetch emails you haven't processed yet, avoiding re-processing on every run.
For real-time lead capture (under 1-minute latency), you need Gmail Push via Cloud Pub/Sub. For batch processing (hourly or daily), polling with messages.list is simpler to implement. The gmail.readonly scope is Restricted — internal Workspace apps don't need OAuth verification, but external apps serving other users' Gmail accounts do. Official docs: https://developers.google.com/workspace/gmail/api
https://gmail.googleapis.com/gmail/v1Setting Up Gmail API Authentication
Gmail lead capture requires the gmail.readonly scope to read inbox messages. This scope is Restricted — you need OAuth verification if external users (outside your organization) will authorize your app. For internal Workspace automation, this requirement is lifted.
- 1Go to https://console.cloud.google.com and create or select a project
- 2Enable Gmail API: APIs & Services → Library → Gmail API → Enable
- 3Configure OAuth consent screen with your app details and contact email
- 4Add scope: https://www.googleapis.com/auth/gmail.readonly
- 5Create OAuth Client ID: Credentials → Create Credentials → OAuth Client ID → Desktop app
- 6Download credentials.json
- 7Run auth script once interactively to complete OAuth consent flow and save token.json
- 8For automated server-side processing: use Service Account + DWD for Workspace domains
1import os2from google.oauth2.credentials import Credentials3from google_auth_oauthlib.flow import InstalledAppFlow4from google.auth.transport.requests import Request5from googleapiclient.discovery import build67SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']89def get_service():10 creds = None11 if os.path.exists('token.json'):12 creds = Credentials.from_authorized_user_file('token.json', SCOPES)13 if not creds or not creds.valid:14 if creds and creds.expired and creds.refresh_token:15 creds.refresh(Request())16 else:17 flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)18 creds = flow.run_local_server(port=0)19 with open('token.json', 'w') as f:20 f.write(creds.to_json())21 return build('gmail', 'v1', credentials=creds)Security notes
- •Store credentials.json and token.json outside version control — add to .gitignore
- •gmail.readonly is Restricted for external apps — complete OAuth verification before deploying to users outside your Workspace
- •Never log or store raw email content — extract only the specific fields (name, email, message) you need
- •For Workspace multi-inbox setups, use Service Account + DWD instead of storing per-user OAuth tokens
- •Implement data retention policies for captured lead data — GDPR requires lawful basis for processing personal data from emails
Key endpoints
/gmail/v1/users/{userId}/messagesLists message IDs matching a search query. Returns only {id, threadId} — requires a separate messages.get call for content. Use after: query operator to only fetch unprocessed messages.
| Parameter | Type | Required | Description |
|---|---|---|---|
q | string | optional | Gmail search syntax: 'from:noreply@typeform.com subject:New response after:2025/01/01' |
maxResults | number | optional | 1-500 messages per page |
pageToken | string | optional | Pagination token from previous response |
labelIds | array | optional | Filter by label IDs (e.g., ['INBOX']) |
includeSpamTrash | boolean | optional | Include Spam and Trash folders |
Response
1{"messages":[{"id":"18c3a2b1d4e5f6a7","threadId":"18c3a2b1d4e5f6a7"},{"id":"18c3a2b1d4e5f699","threadId":"18c3a2b1d4e5f699"}],"nextPageToken":"06614753730951641","resultSizeEstimate":23}/gmail/v1/users/{userId}/messages/{id}Fetches full message content including headers and body. This is where you extract name, email, and message content from lead notification emails.
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | required | Message ID from messages.list |
format | string | optional | 'full' for body content, 'metadata' for headers only |
metadataHeaders | array | optional | Headers to include when format=metadata |
Response
1{"id":"18c3a2b1d4e5f6a7","threadId":"18c3a2b1d4e5f6a7","payload":{"headers":[{"name":"From","value":"noreply@typeform.com"},{"name":"Subject","value":"New response from Jane Doe"}],"parts":[{"mimeType":"text/plain","body":{"data":"TmFtZTogSmFuZSBEb2UKRW1haWw6IGphbmVAZXhhbXBsZS5jb20K"}}]}}/gmail/v1/users/{userId}/watchRegisters Gmail Push notifications for real-time lead capture via Cloud Pub/Sub. Required for sub-minute latency. Watch expires every 7 days maximum.
| Parameter | Type | Required | Description |
|---|---|---|---|
topicName | string | required | Full Pub/Sub topic: projects/{project}/topics/{topic} |
labelIds | array | optional | Notify only for messages with these labels |
labelFilterBehavior | string | optional | INCLUDE or EXCLUDE |
Request
1{"topicName":"projects/my-project/topics/lead-capture","labelIds":["INBOX"],"labelFilterBehavior":"INCLUDE"}Response
1{"historyId":"2054799","expiration":"1749600000000"}/gmail/v1/users/{userId}/historyUsed with Gmail Push — fetches actual changes since a given historyId from a Pub/Sub notification payload.
| Parameter | Type | Required | Description |
|---|---|---|---|
startHistoryId | string | required | historyId from Pub/Sub notification |
historyTypes | array | optional | ['messageAdded'] for new messages only |
labelId | string | optional | Filter to messages with this label |
Response
1{"history":[{"messagesAdded":[{"message":{"id":"18c3a2b1","labelIds":["INBOX","UNREAD"]}}]}],"historyId":"2054802"}Step-by-step automation
List Lead Emails with Search Query
Why: The search query is the filter that separates lead emails from general inbox noise — getting this right determines the quality of your captured leads.
Build a Gmail search query that targets your lead notification emails. Common patterns: form submissions from Typeform/Calendly/JotForm, inquiry emails with specific subjects, emails from your website contact form. Use after: with a Unix timestamp or YYYY/MM/DD format to only fetch emails since your last run. Paginate using nextPageToken until there are no more results.
1# List lead emails from Typeform since Jan 1 20252curl -H "Authorization: Bearer $ACCESS_TOKEN" \3 'https://gmail.googleapis.com/gmail/v1/users/me/messages?q=from:notifications@typeform.com+after:2025/01/01&maxResults=100'45# List contact form emails by subject pattern6curl -H "Authorization: Bearer $ACCESS_TOKEN" \7 'https://gmail.googleapis.com/gmail/v1/users/me/messages?q=subject:"New+contact+form+submission"+in:inbox&maxResults=100'Pro tip: Store the timestamp of your last successful run and use it in the after: query — this prevents re-processing old messages and saves quota. Use Unix epoch seconds: after:1735686000 is equivalent to after:2025/01/01.
Expected result: An array of message IDs. Each ID requires a separate messages.get call in the next step to get the actual email content.
Fetch and Parse Email Content
Why: messages.list only returns IDs — you must fetch each message individually to extract lead information from the email body.
For each message ID, call messages.get with format='full' to get the complete email. Parse the payload to extract plain text body, then use regex or string parsing to find the contact information. Lead notification emails from form services follow predictable formats. Handle both simple text/plain bodies and multipart MIME messages.
1# Fetch full message content (5 quota units)2curl -H "Authorization: Bearer $ACCESS_TOKEN" \3 'https://gmail.googleapis.com/gmail/v1/users/me/messages/18c3a2b1d4e5f6a7?format=full'Pro tip: Request format='metadata' first for all messages to check their subjects and filter by source, then only request format='full' for messages that pass your filter. This cuts your quota cost in half for mixed-content inboxes.
Expected result: An array of lead objects with extracted name, email, company, and message fields. Only messages where an email address was found are included.
Write Extracted Leads to Google Sheets or CRM
Why: Capturing lead data in your inbox is useless without writing it somewhere your team can act on it.
After parsing leads from emails, write them to a Google Sheet via the Sheets API (values.append), post to your CRM via its API, or insert into a database. For Google Sheets, use valueInputOption=USER_ENTERED so dates and numbers are parsed correctly. Check for duplicate emails before appending to avoid recording the same lead twice.
1# Append leads to Google Sheets2curl -X POST \3 -H "Authorization: Bearer $ACCESS_TOKEN" \4 -H "Content-Type: application/json" \5 -d '{6 "values": [7 ["Jane Doe", "jane@example.com", "Acme Corp", "Looking for automation help", "2025-05-07"]8 ]9 }' \10 'https://sheets.googleapis.com/v4/spreadsheets/SPREADSHEET_ID/values/Leads!A:E:append?valueInputOption=USER_ENTERED'Pro tip: Add a timestamp column to every row — it lets you filter by capture date for reporting and helps debug issues when leads appear to be missing.
Expected result: New leads are appended to the Google Sheet. Duplicate emails (already in the sheet) are skipped.
Track Processed Message IDs to Avoid Re-Processing
Why: Running the script daily without tracking processed messages means re-parsing and potentially duplicating leads you already captured.
After successfully processing a batch of messages, store their IDs in a simple JSON file, database table, or Google Sheet. On the next run, filter out already-processed IDs before fetching message content. The most efficient approach is to use the after: search operator with the timestamp of your last run, which filters at the API level before you even fetch message IDs.
1# Use after: to only fetch new emails since last run (most efficient)2curl -H "Authorization: Bearer $ACCESS_TOKEN" \3 'https://gmail.googleapis.com/gmail/v1/users/me/messages?q=from:notifications@typeform.com+after:1746576000&maxResults=100'4# Unix timestamp: 1746576000 = 2025-05-07 00:00:00 UTCPro tip: Using after: with a Unix timestamp at the API level is more efficient than filtering locally — it reduces the number of IDs returned by messages.list, saving quota units.
Expected result: Checkpoint file is updated after each run with the last run timestamp and processed message IDs. Next run only fetches messages after the last run timestamp.
Complete working code
Complete lead capture pipeline: polls Gmail for new form submission notifications, parses lead data from email bodies, deduplicates against a Google Sheet, writes new leads with timestamps, and saves a checkpoint for the next run. Schedule this script via cron (e.g., every 15 minutes) for near-real-time lead capture.
1#!/usr/bin/env python32"""Gmail Lead Capture — polls inbox, parses leads, writes to Google Sheets."""3import os4import base645import json6import re7import time8import logging9from datetime import datetime10from pathlib import Path11from googleapiclient.discovery import build12from google.oauth2.credentials import Credentials13from google_auth_oauthlib.flow import InstalledAppFlow14from google.auth.transport.requests import Request1516logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')17log = logging.getLogger(__name__)1819SCOPES = ['https://www.googleapis.com/auth/gmail.readonly', 'https://www.googleapis.com/auth/spreadsheets']20LEAD_QUERY = os.environ.get('LEAD_QUERY', 'subject:"New contact form" in:inbox')21SPREADSHEET_ID = os.environ.get('SPREADSHEET_ID', 'your-spreadsheet-id')22SHEET_NAME = 'Leads'23CHECKPOINT_FILE = 'checkpoint.json'2425def get_creds():26 creds = None27 if os.path.exists('token.json'):28 creds = Credentials.from_authorized_user_file('token.json', SCOPES)29 if not creds or not creds.valid:30 if creds and creds.expired and creds.refresh_token:31 creds.refresh(Request())32 else:33 flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES)34 creds = flow.run_local_server(port=0)35 with open('token.json', 'w') as f: f.write(creds.to_json())36 return creds3738def get_body(payload):39 if payload.get('mimeType') == 'text/plain' and payload.get('body', {}).get('data'):40 return base64.urlsafe_b64decode(payload['body']['data'] + '==').decode('utf-8', errors='ignore')41 for p in payload.get('parts', []):42 r = get_body(p)43 if r: return r44 return ''4546def parse_lead(body, headers):47 lead = {'received_at': headers.get('Date', '')}48 for field, pattern in [49 ('name', r'(?:Name|Full Name):\s*(.+)'),50 ('email', r'(?:Email):\s*([\w.+-]+@[\w-]+\.[\w.]+)'),51 ('company', r'(?:Company|Org):\s*(.+)'),52 ('message', r'(?:Message|How can we help|Notes):\s*(.+?)(?=\n[A-Z]|$)')53 ]:54 m = re.search(pattern, body, re.IGNORECASE | re.DOTALL)55 if m: lead[field] = m.group(1).strip()[:500] # Limit length56 # Fallback: extract email from From header if not in body57 if not lead.get('email'):58 m = re.search(r'[\w.+-]+@[\w-]+\.[\w.]+', headers.get('Reply-To', '') + headers.get('From', ''))59 if m: lead['email'] = m.group(0)60 return lead6162def main():63 creds = get_creds()64 gmail = build('gmail', 'v1', credentials=creds)65 sheets = build('sheets', 'v4', credentials=creds)6667 cp = json.loads(Path(CHECKPOINT_FILE).read_text()) if Path(CHECKPOINT_FILE).exists() else {}68 last_ts = cp.get('last_ts')69 processed = set(cp.get('processed', []))70 run_ts = int(time.time())7172 query = LEAD_QUERY + (f' after:{last_ts}' if last_ts else '')73 msg_ids, page_token = [], None74 while True:75 params = {'userId': 'me', 'q': query, 'maxResults': 500}76 if page_token: params['pageToken'] = page_token77 r = gmail.users().messages().list(**params).execute()78 msg_ids.extend([m['id'] for m in r.get('messages', [])])79 page_token = r.get('nextPageToken')80 if not page_token: break8182 new_ids = [i for i in msg_ids if i not in processed]83 log.info(f'Found {len(new_ids)} new messages')8485 existing_emails = set()86 ex = sheets.spreadsheets().values().get(87 spreadsheetId=SPREADSHEET_ID, range=f'{SHEET_NAME}!B:B'88 ).execute()89 for row in ex.get('values', []):90 if row: existing_emails.add(row[0].lower())9192 rows = []93 for msg_id in new_ids:94 msg = gmail.users().messages().get(userId='me', id=msg_id, format='full').execute()95 hdrs = {h['name']: h['value'] for h in msg['payload']['headers']}96 lead = parse_lead(get_body(msg['payload']), hdrs)97 email = lead.get('email', '').lower()98 if email and email not in existing_emails:99 rows.append([lead.get('name',''), lead.get('email',''), lead.get('company',''),100 lead.get('message',''), datetime.now().strftime('%Y-%m-%d %H:%M')])101 existing_emails.add(email)102 processed.add(msg_id)103 time.sleep(0.1)104105 if rows:106 sheets.spreadsheets().values().append(107 spreadsheetId=SPREADSHEET_ID, range=f'{SHEET_NAME}!A:E',108 valueInputOption='USER_ENTERED', body={'values': rows}109 ).execute()110 log.info(f'Written {len(rows)} new leads')111112 Path(CHECKPOINT_FILE).write_text(json.dumps({'last_ts': run_ts, 'processed': list(processed)[-5000:]}))113114if __name__ == '__main__':115 main()Error handling
Request had insufficient authentication scopesToken was issued without gmail.readonly scope, or scope was added after the token was created.
Delete token.json and re-run OAuth flow with SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']. Ensure the scope is added to your OAuth consent screen.
No retry — fix OAuth scopes and re-authorize.
rateLimitExceeded / userRateLimitExceededExceeded 15,000 quota units per minute for the user. Processing 500 messages costs ~2,505 units (5 for list + 5 per message). At high volume, you'll hit this limit.
Add time.sleep(0.1) between messages.get calls. Spread processing over multiple minutes for large backlogs. Use after: query to reduce messages processed per run.
Exponential backoff: 2^n seconds (1s, 2s, 4s, 8s, 16s, 32s, max 64s) then retry.
Requested entity was not foundMessage ID no longer exists — the email was deleted or moved to Trash between listing and fetching.
Catch 404 errors in your messages.get loop and skip the message. This is a benign error for lead capture — the lead simply doesn't exist anymore.
No retry — skip the message and continue.
Invalid value for: maxResultsmaxResults exceeds the maximum of 500 per messages.list call.
Cap maxResults at 500 and use pagination with nextPageToken to retrieve more results.
No retry — fix the maxResults value.
Invalid CredentialsAccess token expired (1-hour TTL). In Testing mode, refresh token expires after 7 days.
The Google client library auto-refreshes if you use Credentials correctly. Ensure token.json is writable and the refresh flow is implemented. Publish the OAuth consent screen for tokens that don't expire at 7 days.
Refresh token immediately, retry once.
Rate Limits for Gmail API
| Scope | Limit | Window |
|---|---|---|
| Per user | 15,000 quota units | per minute |
| Per project | 1,200,000 quota units | per minute |
| messages.list cost | 5 quota units | per call |
| messages.get cost | 5 quota units | per message |
1import time2import random3from googleapiclient.errors import HttpError45def get_message_safe(service, message_id, max_retries=5):6 for attempt in range(max_retries):7 try:8 return service.users().messages().get(9 userId='me', id=message_id, format='full'10 ).execute()11 except HttpError as e:12 if e.resp.status == 404:13 return None # Message deleted, skip14 if e.resp.status in [403, 429, 500, 503]:15 wait = min((2 ** attempt) + random.uniform(0, 1), 64)16 time.sleep(wait)17 else:18 raise19 return None- Use after: timestamp in your query to only fetch emails you haven't processed — avoids quota waste on re-processing
- Add 100-200ms delay between messages.get calls when processing batches of 50+ messages
- Request format='metadata' for an initial pass to filter relevant messages, then format='full' only for matches
- Cache messages.list results — the same message ID won't change, so no need to re-fetch it
- Process leads in batches of 50-100 at a time, saving checkpoint after each batch rather than all at the end
Security checklist
- Store OAuth credentials outside version control — add credentials.json and token.json to .gitignore
- gmail.readonly is Restricted for external apps — complete OAuth verification before deploying to external users
- Never log raw email body content — only log the extracted fields (name, email) after parsing
- Implement GDPR-compliant data handling: capture only what you need, store with purpose, honor deletion requests
- Do not store the raw email bodies in your database — extract and discard
- Validate email format of extracted leads before writing to CRM — regex extraction can produce malformed addresses
- Review your lead parsing regex periodically — form service email formats change and can silently capture wrong data
- Audit your checkpoint file for age and purge old processed IDs to prevent unbounded growth
Automation use cases
Multi-Form Lead Aggregator
intermediateCapture leads from multiple form services (Typeform, JotForm, Calendly, Google Forms) into a single Google Sheet using different query patterns per source.
Real-Time Lead Alert
advancedUse Gmail Push + Pub/Sub to capture leads within seconds of arrival and send Slack notifications to your sales team immediately.
CRM Auto-Populate
advancedExtract leads from email and automatically create contacts in HubSpot, Salesforce, or Notion CRM via their respective APIs.
Lead Qualification Filter
intermediateParse lead email content and apply qualification scores (company size, budget, timeline) before writing to CRM, flagging hot leads for immediate follow-up.
No-code alternatives
Don't want to write code? These platforms can automate the same workflows visually.
Zapier
Free tier (100 tasks/month); Starter from $19.99/monthZapier's Gmail New Email trigger with parser action can extract lead fields from email bodies and write to Google Sheets or CRM.
- + Email Parser tool included
- + Direct CRM integrations
- + No code needed
- - Parser requires exact email format
- - Polling delay (1-15 minutes)
- - Task limits on lower tiers
Make (Integromat)
Free (1,000 ops/month); Core from $9/monthMake's Gmail watch module with text parsing operations provides more flexible field extraction than Zapier's parser.
- + Near-real-time with watch
- + Flexible regex parsing
- + Better pricing at scale
- - More complex setup for parsing
- - 1,000 ops/month free tier
- - Still polling-based
n8n
Free self-hosted; Cloud from €20/monthSelf-hosted n8n with Gmail node and Function nodes allows custom parsing logic and direct database writes without API subscription costs.
- + Free self-hosted
- + Custom parsing in JavaScript
- + Direct DB connections
- - Requires server setup
- - No managed infrastructure
- - OAuth setup required
Best practices
- Use the most specific search query you can — narrow queries reduce quota cost and false positives from unrelated emails
- Always implement duplicate detection before writing to CRM — form submissions can trigger multiple notification emails
- Save your processing checkpoint after every batch of 50 messages, not at the end of the run — crashes should not cause re-processing
- Extract email addresses via regex as a fallback if the structured body parsing fails — form services occasionally change their email templates
- Add a 'Source' column to your lead sheet to record which form/campaign the lead came from
- For real-time requirements, invest in Gmail Push + Pub/Sub rather than polling — polling every 15 minutes means leads wait 0-15 minutes before appearing in your CRM
- Validate parsed email addresses with a simple regex before writing — malformed extractions waste CRM records
Ask AI to help
Copy one of these prompts to get a personalized, working implementation.
I'm using the Gmail API in Python to capture leads from form submission notification emails. My messages.list returns IDs correctly, but messages.get returns emails where the body field is empty — payload.body.data is null. The emails are from Typeform. How do I extract the text body from multipart MIME Gmail messages where the content is nested in parts?
Build a Gmail lead capture dashboard. Features: 1) Configure Gmail search queries for different lead sources (Typeform, Calendly, contact forms), 2) Real-time lead feed showing newly captured leads with name, email, company, message, 3) Lead status pipeline (New, Contacted, Qualified, Closed), 4) Export to CSV, 5) Duplicate detection with alert. Use Supabase for lead storage and a Supabase Edge Function to poll Gmail API on a schedule.
Frequently asked questions
Why does messages.list return only IDs and not the full email content?
This is by design — messages.list is optimized for listing large numbers of message IDs efficiently. Each message returned from messages.list costs 5 quota units. Returning full content for 500 messages would be both slow and expensive. You must call messages.get for each ID to get the body. Use format='metadata' for a first pass when you only need headers, and format='full' only when you need body content.
How do I capture leads in real-time (under 1 minute latency)?
Use Gmail Push via Cloud Pub/Sub. Call users.watch to register the inbox, receive push notifications from Pub/Sub when new messages arrive, then call users.history.list to get the new message IDs. This gives you near-instant notification (seconds). The setup is more complex — you need a GCP project, Pub/Sub topic, and a public HTTPS webhook endpoint. For latency of 1-15 minutes, periodic polling with messages.list is simpler.
Is the Gmail API free for lead capture?
The Gmail API itself is free with no per-request charges. The gmail.readonly scope is Restricted for external apps, which means you may need to complete Google's OAuth verification process if you're building for users outside your organization. Internal Workspace apps don't need verification. No per-message or per-quota-unit charges exist.
What happens when I hit the quota limit while processing a large inbox?
You'll receive HTTP 403 with reason 'userRateLimitExceeded'. The per-user limit is 15,000 quota units/minute. Processing 500 messages = 5 (list) + 500×5 (get) = 2,505 units — well within the per-minute limit if done in one minute. For larger volumes, add 100ms delays between messages.get calls and save your checkpoint progress so you can resume if interrupted.
How do I handle emails where the parsed content doesn't match my regex?
Build a fallback chain: 1) Try your primary regex, 2) Try alternative patterns (form services change email templates), 3) Fall back to extracting just the From/Reply-To email address from headers (even if you can't get name/company), 4) Log failed parses with the message ID so you can manually review and update your regex. Never discard a message just because parsing fails — capture what you can.
Can I capture leads from multiple Gmail accounts?
Yes. For Google Workspace organizations, use Service Account + Domain-Wide Delegation to impersonate multiple mailboxes with a single service account. For personal Gmail accounts, each requires its own OAuth consent flow and token. Store separate token files per account and initialize a separate Gmail service for each.
Can RapidDev help build a custom lead capture system?
Yes. RapidDev has built 600+ apps including automated lead capture pipelines integrating Gmail API with CRMs (HubSpot, Salesforce, Notion) and Google Sheets. We can build the complete system including parsing logic for your specific form services, real-time capture via Gmail Push, and a management dashboard. Contact us for a free consultation at rapidevelopers.com.
Need this automated?
Our team has built 600+ apps with API automations. We can build this for you.
Book a free consultation