skills/source/impl/frappe-impl-integrations/SKILL.md
Use when implementing OAuth providers, Connected Apps, Webhooks, Payment Gateways, or Data Import/Export in Frappe. Prevents authentication failures from wrong OAuth flow, missed webhook deliveries, and data corruption during bulk imports. Covers OAuth2 provider/client, Connected App DocType, Webhook DocType, Payment Gateway integration, Data Import, Data Export, frappe.integrations module. Keywords: OAuth, Connected App, Webhook, Payment Gateway, Data Import, Data Export, integration, API key, OAuth2, webhook trigger, connect to external service, OAuth setup, webhook configuration, import data, export data..
npx skillsauth add OpenAEC-Foundation/ERPNext_Anthropic_Claude_Development_Skill_Package frappe-impl-integrationsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Step-by-step workflows for OAuth, Webhooks, Payment Gateways, Data Import/Export, and external API calls.
Version: v14/v15/v16
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)
Use when external applications need "Sign in with Frappe" or API access on behalf of users.
Navigate to Setup > Integrations > OAuth Provider Settings:
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 |
| 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 |
# 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 all
Implicit grant type for server-side apps — use Authorization Codeclient_secret in client-side JavaScriptUse when your Frappe instance needs to access external services (Google, Microsoft, etc.) on behalf of users.
| 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/) |
Copy the auto-generated Redirect URI and register it in the external provider's OAuth console.
access_type=offline # Google: enables refresh tokens
prompt=consent # Google: forces re-consent for refresh token
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")
access_type=offline for Google APIs to get refresh tokensTokenExpiredError — call session.refresh_token() or reconnectNavigate 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 |
Add custom headers for authentication:
Authorization: Bearer <api_token>
Content-Type: application/json
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 }}"
}
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)
| tojson filter for child table data in JSON payloadsfrom 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"}
)
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")
raise
timeout on external requests (30s recommended)frappe.log_error()validate or before_save — use on_update + frappe.enqueue()frappe.enqueue() for slow external calls to avoid blocking the web requestInsert or Update)ID,Item Name,Item Group,Stock UOM
,Widget A,Products,Nos
,Widget B,Raw Material,Kg
ID/name empty for Insert (auto-generated)ID column MUST contain existing document namesimport 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()
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 myapp
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()
# Generate via User > API Access > Generate Keys
curl -H "Authorization: token api_key:api_secret" \
https://your-site.com/api/resource/Sales%20Invoice
curl -H "Authorization: Bearer access_token" \
https://your-site.com/api/resource/Sales%20Invoice
# Login first
curl -X POST https://your-site.com/api/method/login \
-d "[email protected]&pwd=password"
# Subsequent requests use session cookie
| 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() |
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()
| 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 |
| 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 |
development
Use when implementing hooks.py configurations in a Frappe custom app. Covers step-by-step workflows for doc_events, scheduler_events, override/extend_doctype_class, permission hooks, extend_bootinfo, fixtures, asset injection, website hooks, and doctype_js. Prevents broken transactions, missed migrations, and multi-app conflicts. Keywords: hooks.py, doc_events, scheduler_events, override doctype,, how to add hook, when to use doc_events, scheduler setup, override existing behavior. extend doctype class, permission hook, scheduler job, fixtures, doctype_js, extend_bootinfo, website hooks.
development
Use when building a custom Frappe app from scratch. Covers bench new-app walkthrough, app structure decisions, adding DocTypes, hooks, patches, fixtures management, development workflow (bench migrate, build, clear-cache), testing, packaging, installing on another site, version management, and app dependencies for v14/v15/v16. Keywords: create custom app, new frappe app, bench new-app, app structure, module creation, doctype creation, fixtures, patches, deployment, packaging, data migration, patch file, patches.txt, migrate data between DocTypes, create new app from scratch.
development
Use when building Document Controllers in a custom Frappe app: file creation, lifecycle hooks, validation, autoname, submittable workflows, controller override, child table controllers, flags system, migration from hooks.py and Server Scripts. Keywords: how to implement controller, which hook to use, validate vs on_update, override controller, submittable document, autoname, flags, extend_doctype_class, controller testing, child table controller, which hook to use, when does validate run, how to override save, document lifecycle.
tools
Use when implementing client-side form features in Frappe/ERPNext: field visibility, cascading filters, calculated fields, custom buttons, server calls, form validation, child table logic, debugging. Covers step-by-step workflows from Setup > Client Script through migration to custom app JS. Keywords: how to implement client script, form logic workflow, dynamic UI, calculate fields, frm.call, frappe.call, frappe.xcall, client script testing, field dependency, custom button, how to hide field, show field based on value, add button to form, calculate total, dynamic form.