Cet article est actuellement disponible en anglais uniquement. Traduction à venir.
By the end of this recipe, your Odoo 19 instance will send sales orders, master service agreements, and HR offer letters to customers via DocuSign for legally binding e-signature, with completed PDFs returned as Odoo attachments and the originating record auto-progressed to "Signed". Skill required: Odoo developer with OAuth + REST experience. Time required: 4 hours setup, 90 minutes testing. ECOSIRE has built this for SaaS clients running 200+ contracts/month and HR teams hiring at scale, and the recipe below is the playbook we ship.
The reason most DocuSign-Odoo integrations break: people use the JWT (Service-Integration) auth flow without scoping permissions, and once the signing user leaves the company, every existing envelope tied to that user fails. The recipe below uses an Account-Level (System Integration) JWT flow that's tied to the organization, not a user.
What you will need
- DocuSign developer account: free at
developers.docusign.comfor sandbox; production requires a paid eSignature plan. - DocuSign integration key (client ID) + RSA keypair generated in DocuSign Admin > Apps and Keys.
- API account ID and server base URL (na2, na3, eu, etc., depending on data residency).
- Odoo version: 17, 18, or 19. The
signmodule ships with Enterprise, but DocuSign integration is a separate custom layer. - Time: 4 hours setup, 90 minutes testing.
Step-by-step
1. Create the DocuSign integration
In DocuSign Admin > Integrations > Apps and Keys > Add App and Integration Key. Set:
- App name: Odoo Integration
- Authentication: Authorization Code Grant + JWT
- Generate RSA Keypair: download the private key (
.pem) - Redirect URI:
https://your-odoo.com/docusign/oauth/callback - Scopes:
signature,impersonation
Copy the Integration Key (UUID format), Account ID, and API user GUID (your DocuSign user's API GUID, not username). Save the private key as /etc/odoo/docusign-private.pem (chmod 600, owned by odoo).
Verification: in DocuSign, the app shows status "Active" and the key fingerprint is registered.
2. Configure JWT token exchange in Odoo
import jwt as pyjwt
import requests
from datetime import datetime, timedelta
class DocuSignBackend(models.Model):
_name = 'docusign.backend'
name = fields.Char()
integration_key = fields.Char()
api_user_guid = fields.Char()
account_id = fields.Char()
base_url = fields.Char(default='https://demo.docusign.net/restapi') # change to https://www.docusign.net/restapi for prod
private_key_path = fields.Char(default='/etc/odoo/docusign-private.pem')
def _get_access_token(self):
with open(self.private_key_path, 'rb') as f:
private_key = f.read()
now = int(datetime.utcnow().timestamp())
payload = {
'iss': self.integration_key,
'sub': self.api_user_guid,
'iat': now,
'exp': now + 3600,
'aud': 'account-d.docusign.com' if 'demo' in self.base_url else 'account.docusign.com',
'scope': 'signature impersonation',
}
token = pyjwt.encode(payload, private_key, algorithm='RS256')
resp = requests.post(
f"https://account{'-d' if 'demo' in self.base_url else ''}.docusign.com/oauth/token",
data={
'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
'assertion': token,
},
timeout=30,
)
resp.raise_for_status()
return resp.json()['access_token']
The JWT-bearer grant requires user consent the first time — visit https://account-d.docusign.com/oauth/auth?response_type=code&scope=signature%20impersonation&client_id={int_key}&redirect_uri=https://your-odoo.com/docusign/oauth/callback once, sign in, approve. After that JWT works without user interaction.
Verification: _get_access_token() returns a non-empty JWT each time.
3. Send an envelope from a Sale Order
When a sales rep clicks "Send for Signature", generate the contract PDF, base64-encode it, and POST as an envelope:
def _create_envelope_from_so(self, sale_order):
pdf_data = self.env.ref('sale.action_report_saleorder')._render_qweb_pdf([sale_order.id])[0]
pdf_b64 = base64.b64encode(pdf_data).decode()
payload = {
'emailSubject': f'Please sign your quote {sale_order.name}',
'emailBlurb': 'Click below to review and sign electronically.',
'documents': [{
'documentBase64': pdf_b64,
'name': f'Quote_{sale_order.name}.pdf',
'fileExtension': 'pdf',
'documentId': '1',
}],
'recipients': {
'signers': [{
'email': sale_order.partner_id.email,
'name': sale_order.partner_id.name,
'recipientId': '1',
'routingOrder': '1',
'tabs': {
'signHereTabs': [{
'documentId': '1',
'pageNumber': '1',
'xPosition': '380',
'yPosition': '720',
}],
'dateSignedTabs': [{
'documentId': '1',
'pageNumber': '1',
'xPosition': '380',
'yPosition': '760',
}],
},
}],
},
'eventNotification': {
'url': 'https://your-odoo.com/docusign/webhook',
'requireAcknowledgment': 'true',
'envelopeEvents': [
{'envelopeEventStatusCode': 'completed'},
{'envelopeEventStatusCode': 'declined'},
{'envelopeEventStatusCode': 'voided'},
],
'includeDocuments': 'true',
},
'status': 'sent',
}
token = self._get_access_token()
resp = requests.post(
f"{self.base_url}/v2.1/accounts/{self.account_id}/envelopes",
json=payload,
headers={'Authorization': f'Bearer {token}'},
timeout=60,
)
resp.raise_for_status()
envelope_id = resp.json()['envelopeId']
sale_order.docusign_envelope_id = envelope_id
sale_order.docusign_status = 'sent'
return envelope_id
Verification: trigger the send; the customer receives the DocuSign email within 30 seconds.
4. Receive the completion webhook
DocuSign POSTs a Connect webhook on envelope completion (or decline/void). Configure the controller:
@http.route('/docusign/webhook', auth='public', csrf=False, methods=['POST'])
def webhook(self, **kwargs):
body = request.httprequest.get_data()
# Optionally verify HMAC if you set a secret in Connect config
payload = json.loads(body)
envelope = payload.get('data', {}).get('envelopeSummary', {})
envelope_id = envelope.get('envelopeId')
status = envelope.get('status')
so = request.env['sale.order'].sudo().search([('docusign_envelope_id', '=', envelope_id)], limit=1)
if not so:
return request.make_response('No matching SO', status=200)
so.docusign_status = status
if status == 'completed':
# Pull the signed PDF and save as attachment
token = so.docusign_backend_id._get_access_token()
pdf_resp = requests.get(
f"{so.docusign_backend_id.base_url}/v2.1/accounts/{so.docusign_backend_id.account_id}/envelopes/{envelope_id}/documents/combined",
headers={'Authorization': f'Bearer {token}'},
timeout=60,
)
request.env['ir.attachment'].sudo().create({
'name': f'{so.name}_signed.pdf',
'datas': base64.b64encode(pdf_resp.content),
'res_model': 'sale.order',
'res_id': so.id,
'mimetype': 'application/pdf',
})
# Auto-confirm the SO
if so.state == 'sent':
so.action_confirm()
elif status == 'declined':
so.message_post(body='Customer declined the DocuSign envelope.')
return request.make_response('OK', status=200)
Verification: sign the test envelope; the SO progresses to sale state and the signed PDF appears as an attachment.
5. Build a tab-positioning helper
Hardcoded tab coordinates work but break if the report layout changes. Build a helper that finds anchor strings in the PDF and places tabs relative to them. DocuSign supports anchorString natively:
'signHereTabs': [{
'anchorString': '[customer_signature]',
'anchorXOffset': '0',
'anchorYOffset': '0',
'anchorIgnoreIfNotPresent': 'false',
}]
In your Odoo report template, add <span style="color: white;">[customer_signature]</span> where you want the signature. The white text is invisible to humans but DocuSign can find it.
Verification: the signature box renders at the right spot regardless of how long the line items table grows.
6. Use signing templates for repetitive contracts
For NDAs, MSAs, and offer letters that go out repeatedly, create DocuSign Templates with pre-positioned tabs. Reference them by templateId in the envelope payload instead of building tabs manually:
payload = {
'emailSubject': f'Sign your {template_name}',
'templateId': self.docusign_template_id,
'templateRoles': [{
'email': partner.email,
'name': partner.name,
'roleName': 'Signer',
}],
'status': 'sent',
}
Verification: a templated NDA send takes 2 lines of code instead of 30.
7. Track envelope status in Odoo
Add a docusign_status field with selection (sent / delivered / completed / declined / voided / expired). Update via webhook. Add a kanban-style status badge on the SO form view.
Verification: open an SO with an active envelope; the status badge reflects current DocuSign state.
8. Handle multi-signer envelopes
For contracts requiring both customer signature and counter-signature from your team, set two signers with different routingOrder. Customer signs first (routingOrder=1), then your CEO signs (routingOrder=2). DocuSign emails the next signer automatically when the previous completes.
Verification: a 2-signer envelope progresses through both signatures and only fires completed after both done.
Common mistakes
- JWT clock skew. The
iatandexpclaims are strict — server clocks must be in sync (run NTP). - Hardcoded sandbox URL in production. Demo (na-d) and prod (na, na2, na3, eu) have different endpoints.
- Missing
eventNotificationin envelope payload. No webhook fires; you never know the envelope completed. - Storing the PEM key in the database. Always on the file system with strict permissions.
- Not handling envelope expiration. Default 120 days. Set up a cron that flags long-pending envelopes for sales follow-up.
Going further
In-app signing: instead of email-and-redirect, embed DocuSign's signing iframe inside Odoo. Customer signs without leaving your portal. Uses the EmbeddedSigner flow.
Template library by category: build a small admin UI listing all DocuSign templates with search, so sales reps pick the right contract for each deal type.
Auto-archive to S3: instead of attaching to Odoo (which bloats the filestore for high-volume signers), upload completed PDFs to S3 with a presigned link saved on the SO.
Counter-signature workflow: for multi-step approval (sales rep > sales manager > legal > customer), use DocuSign's routing order to chain approvals.
For full DocuSign + Odoo deployment including template library, embedded signing, and contract analytics, ECOSIRE custom Odoo development ships fixed-price engagements. Pair this with how to add a custom report in Odoo using external_layout for the contract template generation.
Frequently Asked Questions
What does DocuSign cost?
Personal: $10/month, 5 envelopes/month. Business Pro: $40/user/month for unlimited envelopes. Enterprise: custom pricing for high-volume + advanced features.
Can I use Odoo Sign instead?
Yes for internal use cases (offer letters, time-off, equipment receipt). Odoo Sign is bundled with Enterprise. For customer-facing contracts that need legal weight (eIDAS, ESIGN Act), DocuSign or Adobe Sign provides better audit trails and is more recognized in enterprise procurement.
How do I handle wet signatures for jurisdictions that don't accept e-signatures?
Some countries (a few in MENA, parts of LATAM) still require wet signature for specific document types. Use DocuSign's "Witnessed Signature" or fall back to printed-and-couriered for those.
What is the legal weight of a DocuSign signature?
DocuSign produces a Certificate of Completion with IP address, timestamp, and signer auth method, attached to every signed PDF. This is admissible in most jurisdictions under ESIGN Act (US), eIDAS (EU), and similar frameworks. Always check local law for specific document types (deeds, wills).
For complex contract automation including approval workflows, redlining, and post-signature obligation tracking, ECOSIRE Odoo customization builds the entire CLM stack. Or read how to integrate Mailchimp with Odoo Marketing for the upstream lead-nurture layer.
Rédigé par
ECOSIRE TeamTechnical Writing
The ECOSIRE technical writing team covers Odoo ERP, Shopify eCommerce, AI agents, Power BI analytics, GoHighLevel automation, and enterprise software best practices. Our guides help businesses make informed technology decisions.
ECOSIRE
Transformez votre entreprise avec Odoo ERP
Implémentation, personnalisation et assistance expertes d'Odoo pour rationaliser vos opérations.
Articles connexes
Comment ajouter un bouton personnalisé à une vue de formulaire Odoo (2026)
Ajoutez des boutons d'action personnalisés aux vues de formulaire Odoo 19 : méthode d'action Python, héritage des vues, visibilité conditionnelle, boîtes de dialogue de confirmation. Testé en production.
Comment ajouter un champ personnalisé dans Odoo sans Studio (2026)
Ajoutez des champs personnalisés via le module personnalisé dans Odoo 19 : héritage de modèle, extension de vue, champs calculés, décisions magasin/non-magasin. Code d'abord, contrôle de version.
Comment ajouter un rapport personnalisé dans Odoo à l'aide d'une mise en page externe
Créez un rapport PDF de marque dans Odoo 19 à l'aide de web.external_layout : modèle QWeb, format papier, liaison d'action. Avec logo imprimé + remplacements de pied de page.