
Frappe Impl Integrations
- 26 installs
- 159 repo stars
- Updated July 8, 2026
- openaec-foundation/frappe_claude_skill_package
Helps with ai & agent building tasks.
About
frappe-impl-integrations is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- frappe-impl-integrations
- AI & Agent Building
- AI-coding skill
Frappe Impl Integrations by the numbers
- 26 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #9,702 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/frappe_claude_skill_package --skill frappe-impl-integrationsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 26 |
|---|---|
| repo stars | ★ 159 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/frappe_claude_skill_package ↗ |
What it does
Helps with ai & agent building tasks.
Files
Frappe Integrations
Step-by-step workflows for OAuth, Webhooks, Payment Gateways, Data Import/Export, and external API calls.
Version: v14/v15/v16
---
Decision Tree: Which Integration Pattern?
WHAT ARE YOU INTEGRATING?
│
├─► External service needs to call YOUR Frappe site?
│ ├─► On document events → Webhook (push to external)
│ ├─► External sends data to you → Whitelisted API endpoint
│ └─► External needs user auth → OAuth 2.0 Provider
│
├─► YOUR Frappe site calls an external service?
│ ├─► Needs user-level OAuth consent → Connected App
│ ├─► Server-to-server with API key → make_request / requests
│ └─► Recurring sync → Scheduler + API calls
│
├─► Bulk data in/out?
│ ├─► Import CSV/XLSX → Data Import DocType
│ ├─► Export data → Report Builder / export-csv / API
│ └─► Programmatic bulk → frappe.get_doc().insert()
│
├─► Payment processing?
│ └─► Payment Request + Payment Gateway controller
│
└─► Real-time vs batch?
├─► Real-time → Webhook or API endpoint
├─► Near real-time → frappe.enqueue() after event
└─► Batch → Scheduler task (hourly/daily)---
Workflow 1: OAuth 2.0: Frappe as Provider
Use when external applications need "Sign in with Frappe" or API access on behalf of users.
Step 1: Configure OAuth Provider Settings
Navigate to Setup > Integrations > OAuth Provider Settings:
- Force: ALWAYS asks user for confirmation
- Auto: Asks only if no active token exists
Step 2: Create OAuth Client
Navigate to Setup > Integrations > OAuth Client:
| Field | Value |
|---|---|
| App Name | External app identifier |
| Scopes | Space-separated (e.g., openid all) |
| Redirect URIs | Space-separated callback URLs |
| Default Redirect URI | Primary callback URL |
| Grant Type | Authorization Code (RECOMMENDED) or Implicit |
| Response Type | Code (for Auth Code) or Token (for Implicit) |
| Skip Authorization | Check for trusted first-party apps only |
Step 3: Use the Generated Endpoints
| Endpoint | URL |
|---|---|
| Authorize | /api/method/frappe.integrations.oauth2.authorize |
| Token | /api/method/frappe.integrations.oauth2.get_token |
| Profile | /api/method/frappe.integrations.oauth2.openid_profile |
Step 4: Configure External App
# Example: Grafana generic_oauth config
client_id = <generated_client_id>
client_secret = <generated_client_secret>
auth_url = https://your-frappe.com/api/method/frappe.integrations.oauth2.authorize
token_url = https://your-frappe.com/api/method/frappe.integrations.oauth2.get_token
api_url = https://your-frappe.com/api/method/frappe.integrations.oauth2.openid_profile
scopes = openid allCritical Rules
- NEVER use
Implicitgrant type for server-side apps — useAuthorization Code - ALWAYS use HTTPS in production for all OAuth endpoints
- NEVER expose
client_secretin client-side JavaScript
---
Workflow 2: Connected App: Frappe as OAuth Consumer
Use when your Frappe instance needs to access external services (Google, Microsoft, etc.) on behalf of users.
Step 1: Create Connected App DocType
| Field | Purpose |
|---|---|
| Name | Identifier for the connection |
| OpenID Configuration URL | Auto-fetches endpoints (e.g., /.well-known/openid-configuration) |
| Authorization URI | Consent screen URL (auto-filled from OpenID) |
| Token URI | Token exchange URL (auto-filled from OpenID) |
| Redirect URI | Auto-generated — copy this to external provider |
| Client ID | From external provider |
| Client Secret | From external provider |
| Scopes | Permissions needed (e.g., https://mail.google.com/) |
Step 2: Register Redirect URI with Provider
Copy the auto-generated Redirect URI and register it in the external provider's OAuth console.
Step 3: Add Extra Parameters (if needed)
access_type=offline # Google: enables refresh tokens
prompt=consent # Google: forces re-consent for refresh tokenStep 4: Use in Code
import frappe
connected_app = frappe.get_doc("Connected App", "My Google App")
# Initiates OAuth flow — user clicks "Connect to..." button
# After consent, tokens are stored automatically
# Making authenticated calls:
session = connected_app.get_oauth2_session()
response = session.get("https://www.googleapis.com/gmail/v1/users/me/messages")Critical Rules
- ALWAYS add
access_type=offlinefor Google APIs to get refresh tokens - NEVER store tokens manually — Connected App manages token lifecycle
- ALWAYS handle
TokenExpiredError— callsession.refresh_token()or reconnect
---
Workflow 3: Webhooks: Push Notifications to External Services
Step 1: Create Webhook DocType
Navigate to Integrations > Webhook:
| Field | Value |
|---|---|
| DocType | Target document type |
| Doc Event | on_update, after_insert, on_submit, on_cancel, on_trash |
| Request URL | External endpoint |
| Request Method | POST (default) |
| Conditions | Optional Jinja filter (e.g., doc.status == "Approved") |
| Enabled | Check to activate |
Step 2: Configure Headers
Add custom headers for authentication:
Authorization: Bearer <api_token>
Content-Type: application/jsonStep 3: Configure Data: Choose Format
Form URL-encoded: Select specific fields from a table.
JSON: Use Jinja templates for structured payloads:
{
"id": "{{ doc.name }}",
"total": "{{ doc.grand_total }}",
"items": {{ doc.items | tojson }},
"event": "{{ event }}"
}Step 4: Enable Webhook Secret (HMAC Verification)
Set a Webhook Secret — Frappe adds X-Frappe-Webhook-Signature header with base64-encoded HMAC-SHA256 hash of the payload.
Receiver verification (Python example):
import hmac, hashlib, base64
def verify_webhook(payload_body, secret, signature_header):
expected = base64.b64encode(
hmac.new(secret.encode(), payload_body, hashlib.sha256).digest()
).decode()
return hmac.compare_digest(expected, signature_header)Critical Rules
- ALWAYS enable Webhook Secret for production webhooks
- NEVER rely on webhooks for guaranteed delivery — implement idempotency on the receiver
- ALWAYS use
| tojsonfilter for child table data in JSON payloads - Webhook logs are created for every delivery — check Webhook Request Log for debugging
---
Workflow 4: External API Calls from Frappe
Using frappe.integrations.utils
from frappe.integrations.utils import make_get_request, make_post_request
# GET request
response = make_get_request(
"https://api.example.com/data",
headers={"Authorization": "Bearer token123"}
)
# POST request
response = make_post_request(
"https://api.example.com/submit",
data={"key": "value"},
headers={"Content-Type": "application/json"}
)Using requests Library Directly
import requests
import frappe
def sync_to_external():
try:
response = requests.post(
"https://api.example.com/endpoint",
json={"data": "value"},
timeout=30
)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
frappe.log_error(f"API call failed: {e}", "Integration Error")
raiseCritical Rules
- ALWAYS set a
timeouton external requests (30s recommended) - ALWAYS wrap external calls in try/except and log errors with
frappe.log_error() - NEVER call external APIs inside
validateorbefore_save— useon_update+frappe.enqueue() - ALWAYS use
frappe.enqueue()for slow external calls to avoid blocking the web request
---
Workflow 5: Data Import
Via UI (Data Import DocType)
1. Navigate to Home > Data Import > New 2. Select DocType and Import Type (Insert or Update) 3. Download template CSV/XLSX 4. Fill in data following the template format 5. Upload and preview 6. Start Import
CSV Format Rules
ID,Item Name,Item Group,Stock UOM
,Widget A,Products,Nos
,Widget B,Raw Material,Kg- First row: field labels or API field names
- Leave
ID/nameempty for Insert (auto-generated) - For Update:
IDcolumn MUST contain existing document names - Child tables: repeat parent row data, add child fields as extra columns
Programmatic Import
import frappe
from frappe.core.doctype.data_import.data_import import DataImport
# Create Data Import document
di = frappe.get_doc({
"doctype": "Data Import",
"reference_doctype": "Item",
"import_type": "Insert New Records",
"import_file": "/path/to/file.csv"
})
di.insert()
di.start_import()Critical Rules
- ALWAYS download and use the template — column order and names must match exactly
- NEVER import more than 5,000 rows at once — split into batches
- ALWAYS test with 5-10 rows first before bulk import
- ALWAYS check Import Log for row-level errors after import completes
---
Workflow 6: Data Export
Via Report Builder
1. Open any DocType list view 2. Apply filters 3. Menu > Export (CSV/Excel)
Via CLI
bench --site mysite export-csv "Sales Invoice"
bench --site mysite export-doc "Sales Invoice" "INV-001"
bench --site mysite export-json "Sales Invoice" "INV-001"
bench --site mysite export-fixtures --app myappProgrammatic Export
import frappe
# Export filtered data
data = frappe.get_all("Sales Invoice",
filters={"status": "Paid", "posting_date": [">", "2024-01-01"]},
fields=["name", "customer", "grand_total", "posting_date"],
order_by="posting_date desc",
limit_page_length=0 # No limit
)
# Convert to CSV
import csv, io
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=["name", "customer", "grand_total", "posting_date"])
writer.writeheader()
writer.writerows(data)
csv_content = output.getvalue()---
Workflow 7: Frappe REST API Authentication
API Key + Secret (Server-to-Server)
# Generate via User > API Access > Generate Keys
curl -H "Authorization: token api_key:api_secret" \
https://your-site.com/api/resource/Sales%20InvoiceOAuth Bearer Token
curl -H "Authorization: Bearer access_token" \
https://your-site.com/api/resource/Sales%20InvoiceSession-Based (Login)
# Login first
curl -X POST https://your-site.com/api/method/login \
-d "usr=user@example.com&pwd=password"
# Subsequent requests use session cookie---
Integration Patterns: Sync vs Async
| Pattern | When to Use | Implementation |
|---|---|---|
| Synchronous | Response needed immediately | Direct API call in controller |
| Async (enqueue) | External call > 5s | frappe.enqueue("myapp.api.sync_record", doc_name=doc.name) |
| Webhook | Push on event | Webhook DocType configuration |
| Scheduled sync | Periodic batch | scheduler_events in hooks.py |
| Real-time | Live updates | Socket.IO + frappe.publish_realtime() |
Retry Pattern
import frappe
from frappe.utils.background_jobs import get_jobs
def sync_with_retry(doc_name, retry_count=0, max_retries=3):
try:
result = call_external_api(doc_name)
frappe.db.set_value("Sales Invoice", doc_name, "sync_status", "Success")
frappe.db.commit()
except Exception as e:
if retry_count < max_retries:
frappe.enqueue(
"myapp.integrations.sync_with_retry",
doc_name=doc_name,
retry_count=retry_count + 1,
queue="short",
enqueue_after_commit=True
)
else:
frappe.log_error(f"Sync failed after {max_retries} retries: {e}")
frappe.db.set_value("Sales Invoice", doc_name, "sync_status", "Failed")
frappe.db.commit()---
Version Differences
| Feature | V14 | V15 | V16 |
|---|---|---|---|
| Webhook DocType | Yes | Yes | Yes |
| Connected App | Yes | Yes | Yes |
| OAuth 2.0 Provider | Yes | Yes | Yes |
| Data Import (new UI) | Yes | Yes | Yes |
| Print Designer | No | Yes | Yes |
make_get_request | Yes | Yes | Yes |
| Webhook HMAC | Yes | Yes | Yes |
---
Reference Files
| File | Contents |
|---|---|
| workflows.md | Complete integration workflow patterns |
| examples.md | Working code examples for all integration types |
| anti-patterns.md | Common integration mistakes and fixes |
| decision-tree.md | Extended decision trees for integration choice |
Integration Anti-Patterns
Anti-Pattern 1: External API Call in validate
# WRONG — blocks save, causes timeout on slow APIs
def validate(doc, method=None):
response = requests.post("https://api.example.com/check", json={"id": doc.name})
if response.json()["status"] != "valid":
frappe.throw("External validation failed")Fix: Move external calls to on_update with frappe.enqueue():
def on_update(doc, method=None):
frappe.enqueue(
"myapp.integrations.validate_external",
doc_name=doc.name,
queue="short",
enqueue_after_commit=True
)Anti-Pattern 2: No Timeout on External Requests
# WRONG — hangs indefinitely if external service is down
response = requests.get("https://api.example.com/data")Fix: ALWAYS set timeout:
response = requests.get("https://api.example.com/data", timeout=30)Anti-Pattern 3: No Error Logging for Failed API Calls
# WRONG — silent failure, impossible to debug
try:
response = requests.post(url, json=data)
except:
passFix: Log ALL integration errors:
try:
response = requests.post(url, json=data, timeout=30)
response.raise_for_status()
except requests.exceptions.RequestException as e:
frappe.log_error(
f"API call to {url} failed: {e}\nPayload: {data}",
"Integration Error"
)
raiseAnti-Pattern 4: Webhook Without HMAC Verification
# WRONG — anyone can send fake webhooks to your endpoint
@frappe.whitelist(allow_guest=True)
def webhook_handler():
data = frappe.parse_json(frappe.request.get_data(as_text=True))
process(data) # No verification!Fix: ALWAYS verify webhook signatures:
@frappe.whitelist(allow_guest=True)
def webhook_handler():
verify_hmac_signature(frappe.request) # Verify FIRST
data = frappe.parse_json(frappe.request.get_data(as_text=True))
process(data)Anti-Pattern 5: No Idempotency on Webhook Receiver
# WRONG — duplicate webhooks create duplicate records
@frappe.whitelist(allow_guest=True)
def handle_payment():
data = frappe.parse_json(frappe.request.get_data(as_text=True))
frappe.get_doc({"doctype": "Payment Entry", ...}).insert()Fix: Check for duplicates using external transaction ID:
@frappe.whitelist(allow_guest=True)
def handle_payment():
data = frappe.parse_json(frappe.request.get_data(as_text=True))
if frappe.db.exists("Payment Entry", {"custom_external_id": data["txn_id"]}):
return {"status": "already_processed"}
frappe.get_doc({"doctype": "Payment Entry", "custom_external_id": data["txn_id"], ...}).insert()Anti-Pattern 6: Storing Secrets in Code
# WRONG — secrets visible in version control
API_KEY = "sk_live_abc123"
response = requests.get(url, headers={"Authorization": f"Bearer {API_KEY}"})Fix: Store secrets in site_config or use Frappe's password field:
api_key = frappe.utils.password.get_decrypted_password(
"My Integration Settings", "My Integration Settings", "api_key"
)
# Or from site_config:
api_key = frappe.conf.get("my_integration_api_key")Anti-Pattern 7: Importing Too Many Rows at Once
# WRONG — memory exhaustion, timeout
# Uploading 50,000 row CSV into Data ImportFix: Split into batches of 5,000 or fewer. For programmatic imports, commit every 100 rows:
for i, row in enumerate(rows):
doc = frappe.get_doc({"doctype": "Item", **row})
doc.insert()
if i % 100 == 0:
frappe.db.commit()
frappe.db.commit()Anti-Pattern 8: OAuth Client Secret in Frontend
// WRONG — client_secret exposed to all users
fetch("/api/method/frappe.integrations.oauth2.get_token", {
body: JSON.stringify({
client_secret: "my_secret_123" // Visible in DevTools!
})
});Fix: Token exchange MUST happen server-side. Use a whitelisted method as proxy.
Anti-Pattern 9: Not Using enqueue_after_commit
# WRONG — enqueued job may run before transaction commits
def on_update(doc, method=None):
frappe.enqueue("myapp.sync.push", doc_name=doc.name)
# Job might start before doc changes are committed!Fix: Use enqueue_after_commit=True:
def on_update(doc, method=None):
frappe.enqueue("myapp.sync.push", doc_name=doc.name, enqueue_after_commit=True)Integration Decision Trees
Decision Tree 1: Webhook vs API vs Scheduled Sync
HOW OFTEN DOES DATA CHANGE?
│
├─► On every document event (real-time)
│ ├─► Push to external? → Webhook DocType
│ ├─► Receive from external? → Whitelisted API endpoint
│ └─► Both directions? → Webhook OUT + API endpoint IN
│
├─► Periodically (hourly, daily)
│ ├─► < 100 records per batch → Scheduler task (hourly)
│ ├─► 100-10,000 records → Scheduler task (daily_long)
│ └─► > 10,000 records → Split into chunks + frappe.enqueue()
│
└─► On demand (user-triggered)
├─► Single record → Button + frappe.call() to whitelisted method
└─► Bulk operation → Background job via frappe.enqueue()Decision Tree 2: Authentication Method
WHO IS CALLING THE API?
│
├─► External service (server-to-server)
│ ├─► Needs user context? → OAuth 2.0 Bearer Token
│ └─► System-level access? → API Key + Secret
│
├─► External app on behalf of user
│ ├─► Web app (server-side) → OAuth Authorization Code
│ ├─► SPA (client-side) → OAuth Authorization Code + PKCE
│ └─► Trusted first-party → Skip Authorization enabled
│
├─► Your Frappe calling external service
│ ├─► Needs user consent (Google, etc.) → Connected App
│ ├─► Server API key → Store in site_config or Password field
│ └─► No auth needed → Direct requests call
│
└─► Browser/portal user
└─► Session-based authentication (login endpoint)Decision Tree 3: Data Import Method
HOW MUCH DATA?
│
├─► 1-100 rows
│ ├─► One-time → Data Import UI (manual upload)
│ └─► Recurring → API calls from external system
│
├─► 100-5,000 rows
│ ├─► CSV/XLSX available → Data Import DocType
│ └─► From API → Programmatic import with batch commits
│
├─► 5,000-50,000 rows
│ ├─► Split into batches of 5,000
│ └─► Use frappe.enqueue() for each batch
│
└─► 50,000+ rows
├─► Direct database insert (advanced, skip validation)
└─► ALWAYS backup before direct DB operationsDecision Tree 4: Error Handling Strategy
WHAT FAILED?
│
├─► External API returned error
│ ├─► 4xx (client error) → Log error, do NOT retry
│ ├─► 5xx (server error) → Retry with exponential backoff
│ ├─► Timeout → Retry once, then log
│ └─► Connection refused → Log, alert admin, skip
│
├─► Webhook delivery failed
│ ├─► Check Webhook Request Log
│ ├─► Verify URL is accessible
│ ├─► Verify payload format matches receiver expectations
│ └─► No auto-retry — implement manual retry or scheduled recheck
│
├─► OAuth token expired
│ ├─► Connected App → Automatic refresh (if refresh_token exists)
│ ├─► No refresh token → User must re-authorize
│ └─► Provider revoked access → User must re-authorize
│
└─► Data Import failed
├─► Check Import Log for row-level errors
├─► Common: missing mandatory fields, duplicate names
├─► Fix CSV and re-import failed rows only
└─► NEVER re-import successful rows (duplicates!)Decision Tree 5: Sync Direction
WHICH DIRECTION?
│
├─► Frappe → External (push)
│ ├─► Real-time → Webhook or doc_event + enqueue
│ ├─► Batch → Scheduler task
│ └─► On demand → Button + whitelisted method
│
├─► External → Frappe (pull/receive)
│ ├─► External pushes → Whitelisted API endpoint
│ ├─► Frappe pulls → Scheduler + external API calls
│ └─► File-based → Data Import (CSV upload)
│
└─► Bidirectional
├─► Use last_modified timestamps to detect changes
├─► Implement conflict resolution (last-write-wins or merge)
└─► ALWAYS log sync direction per record for debuggingIntegration Examples — Working Code
Example 1: OAuth Client Configuration for Grafana
# /etc/grafana/grafana.ini — [auth.generic_oauth] section
[auth.generic_oauth]
enabled = True
name = Frappe
client_id = a1b2c3d4e5f6
client_secret = secretkey123
scopes = openid all
auth_url = https://erp.example.com/api/method/frappe.integrations.oauth2.authorize
token_url = https://erp.example.com/api/method/frappe.integrations.oauth2.get_token
api_url = https://erp.example.com/api/method/frappe.integrations.oauth2.openid_profileExample 2: Connected App for Google APIs
import frappe
def get_google_calendar_events():
"""Fetch Google Calendar events via Connected App."""
connected_app = frappe.get_doc("Connected App", "Google Calendar")
session = connected_app.get_oauth2_session()
response = session.get(
"https://www.googleapis.com/calendar/v3/calendars/primary/events",
params={"maxResults": 10, "orderBy": "startTime", "singleEvents": True}
)
if response.status_code == 200:
return response.json().get("items", [])
else:
frappe.log_error(f"Google Calendar API error: {response.text}")
return []Example 3: Webhook with JSON Payload
Webhook DocType Configuration:
- DocType: Sales Invoice
- Doc Event: on_submit
- Request URL: https://hooks.slack.com/services/T00/B00/XXX
- Webhook Data (JSON):
{
"text": "New Invoice {{ doc.name }} submitted",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Invoice {{ doc.name }}*\nCustomer: {{ doc.customer }}\nTotal: {{ doc.currency }} {{ doc.grand_total }}"
}
}
]
}Example 4: HMAC Webhook Verification (Receiver Side)
# Flask example — receiving Frappe webhooks
import hmac
import hashlib
import base64
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "my-shared-secret"
@app.route("/webhook", methods=["POST"])
def handle_webhook():
signature = request.headers.get("X-Frappe-Webhook-Signature")
if not signature:
abort(401, "Missing signature")
expected = base64.b64encode(
hmac.new(
WEBHOOK_SECRET.encode("utf-8"),
request.get_data(),
hashlib.sha256
).digest()
).decode("utf-8")
if not hmac.compare_digest(expected, signature):
abort(401, "Invalid signature")
data = request.get_json()
# Process webhook data
return {"status": "ok"}, 200Example 5: External API Call with Retry via enqueue
import frappe
import requests
@frappe.whitelist()
def sync_invoice_to_accounting(invoice_name):
"""Queue external sync — NEVER call directly from validate/save."""
frappe.enqueue(
"_do_sync",
invoice_name=invoice_name,
queue="short",
timeout=60,
enqueue_after_commit=True
)
def _do_sync(invoice_name, retry_count=0):
doc = frappe.get_doc("Sales Invoice", invoice_name)
payload = {
"reference": doc.name,
"amount": doc.grand_total,
"customer": doc.customer,
"date": str(doc.posting_date)
}
try:
response = requests.post(
"https://accounting.example.com/api/invoices",
json=payload,
headers={"Authorization": "Bearer " + get_api_token()},
timeout=30
)
response.raise_for_status()
frappe.db.set_value("Sales Invoice", invoice_name,
"custom_external_id", response.json()["id"])
frappe.db.commit()
except requests.exceptions.RequestException as e:
if retry_count < 3:
frappe.enqueue(
"_do_sync",
invoice_name=invoice_name,
retry_count=retry_count + 1,
queue="short",
timeout=60
)
else:
frappe.log_error(
f"Sync failed for {invoice_name} after 3 retries: {e}",
"Accounting Sync Error"
)
def get_api_token():
return frappe.utils.password.get_decrypted_password(
"Integration Settings", "Integration Settings", "api_token"
)Example 6: Data Import via Code
import frappe
def import_items_from_csv(file_path):
"""Programmatic data import with error handling."""
di = frappe.get_doc({
"doctype": "Data Import",
"reference_doctype": "Item",
"import_type": "Insert New Records",
})
di.insert()
# Attach file
file_doc = frappe.get_doc({
"doctype": "File",
"file_url": file_path,
"attached_to_doctype": "Data Import",
"attached_to_name": di.name
})
file_doc.insert()
di.import_file = file_doc.file_url
di.save()
di.start_import()
# Check results
frappe.db.commit()
di.reload()
return {
"total": di.payload_count,
"success": di.import_log_count,
"status": di.status
}Example 7: Bulk Export to CSV
import frappe
import csv
import io
@frappe.whitelist()
def export_customers_csv():
"""Export all active customers as CSV."""
customers = frappe.get_all("Customer",
filters={"disabled": 0},
fields=["name", "customer_name", "customer_group", "territory",
"default_currency", "creation"],
order_by="customer_name asc",
limit_page_length=0
)
output = io.StringIO()
writer = csv.DictWriter(output, fieldnames=[
"name", "customer_name", "customer_group", "territory",
"default_currency", "creation"
])
writer.writeheader()
writer.writerows(customers)
# Save as file
content = output.getvalue()
file_doc = frappe.get_doc({
"doctype": "File",
"file_name": "customers_export.csv",
"content": content,
"is_private": 1
})
file_doc.save()
return file_doc.file_urlExample 8: Inbound Webhook Handler
# myapp/api.py
import frappe
import hmac
import hashlib
import base64
@frappe.whitelist(allow_guest=True)
def receive_payment_webhook():
"""Handle inbound webhook from payment provider."""
# Verify HMAC
secret = frappe.db.get_single_value("Payment Settings", "webhook_secret")
signature = frappe.request.headers.get("X-Signature-256")
payload = frappe.request.get_data()
expected = base64.b64encode(
hmac.new(secret.encode(), payload, hashlib.sha256).digest()
).decode()
if not hmac.compare_digest(expected, signature or ""):
frappe.throw("Invalid webhook signature", frappe.AuthenticationError)
data = frappe.parse_json(frappe.request.get_data(as_text=True))
# Idempotency check
if frappe.db.exists("Payment Log", {"external_id": data["transaction_id"]}):
return {"status": "already_processed"}
# Process payment
frappe.get_doc({
"doctype": "Payment Log",
"external_id": data["transaction_id"],
"amount": data["amount"],
"status": data["status"]
}).insert(ignore_permissions=True)
frappe.db.commit()
return {"status": "ok"}Integration Workflows — Extended Reference
OAuth 2.0 Provider: Complete Authorization Code Flow
1. External app redirects user to:
GET /api/method/frappe.integrations.oauth2.authorize
?client_id=<client_id>
&redirect_uri=<callback_url>
&response_type=code
&scope=openid all
2. User authenticates with Frappe credentials
3. User grants consent (unless Skip Authorization is enabled)
4. Frappe redirects to callback with authorization code:
GET <callback_url>?code=<authorization_code>
5. External app exchanges code for token:
POST /api/method/frappe.integrations.oauth2.get_token
Body: {
grant_type: authorization_code,
code: <authorization_code>,
redirect_uri: <callback_url>,
client_id: <client_id>,
client_secret: <client_secret>
}
6. Frappe returns access_token + refresh_token
7. External app uses token for API calls:
GET /api/resource/Sales Invoice
Authorization: Bearer <access_token>Connected App: Token Refresh Flow
1. Access token expires (typically 1 hour)
2. Connected App detects expired token
3. Automatic refresh using stored refresh_token
4. New access_token issued
5. API call retried with new token
If refresh_token also expired:
→ User must re-authorize via "Connect to..." buttonWebhook Delivery Flow
1. Document event fires (e.g., on_submit on Sales Invoice)
2. Frappe checks active Webhooks for matching DocType + Event
3. Conditions evaluated (Jinja expression)
4. If conditions pass:
a. Payload constructed (Form or JSON format)
b. HMAC signature computed (if secret configured)
c. HTTP request sent to Request URL
d. Webhook Request Log created with response
5. If delivery fails:
a. Error logged in Webhook Request Log
b. No automatic retry (implement retry externally)Data Import Flow
1. Create Data Import document
2. Select Reference DocType
3. Choose Import Type:
- Insert New Records: creates new documents
- Update Existing Records: updates by name/ID
4. Download Template (preserves column order)
5. Fill data in template
6. Upload file
7. Preview imported data
8. Start Import (background job)
9. Check Import Log for results:
- Success: document created/updated
- Warning: partial success
- Error: row skipped with error messagePayment Request Flow
1. Create Payment Request (linked to Sales Invoice/Order)
2. Payment Request selects Payment Gateway
3. User redirected to gateway payment page
4. User completes payment
5. Gateway sends callback to Frappe
6. Payment controller processes callback:
a. Validates payment signature
b. Creates Payment Entry
c. Updates Payment Request status
d. Optionally submits linked documentScheduled Sync Pattern
# hooks.py
scheduler_events = {
"hourly": ["myapp.integrations.sync.hourly_sync"],
"daily": ["myapp.integrations.sync.daily_full_sync"]
}
# myapp/integrations/sync.py
import frappe
def hourly_sync():
"""Sync recent changes only."""
last_sync = frappe.db.get_single_value("My Integration Settings", "last_sync_time")
records = frappe.get_all("Sales Invoice",
filters={"modified": [">", last_sync]},
fields=["name", "customer", "grand_total"]
)
for record in records:
try:
push_to_external(record)
except Exception as e:
frappe.log_error(f"Sync failed for {record.name}: {e}")
frappe.db.set_single_value("My Integration Settings", "last_sync_time",
frappe.utils.now_datetime())
frappe.db.commit()
def daily_full_sync():
"""Full reconciliation — runs as daily_long."""
# Use daily_long in hooks.py if > 5 minutes
pass