All guides
Workify
Twenty CRM
CRMadvanced

Connect Twenty CRM to Workify via Webhooks

Twenty is the open-source CRM built for developers. Learn how to trigger Workify invoice creation directly from Twenty's webhook system when opportunities are won.

What You'll Build

A direct webhook integration between Twenty CRM and Workify. When an opportunity in Twenty is moved to Won, Twenty fires a webhook that creates a Workify invoice — no third-party automation platform required.

Why Twenty + Workify

Twenty is an open-source CRM (think Salesforce, but self-hosted and developer-friendly) with a GraphQL API and a clean webhook system. Workify exposes a REST API. Both are built for developers who want control over their tooling without vendor lock-in. Connecting them is a natural fit.

What You'll Need

  • Twenty CRM (self-hosted or Twenty Cloud)
  • A Workify Pro account with an API key (Settings → API Keys)
  • A small serverless function or proxy (Cloudflare Workers, Vercel Edge Function, or similar) to transform the webhook payload — or you can use Make/n8n as middleware

Understanding Twenty's Webhook System

Twenty fires webhooks for object mutations. You can subscribe to events like:

  • opportunity.created
  • opportunity.updated
  • person.created

Each webhook delivers a JSON payload with the changed object and its fields. You'll listen for opportunity.updated and filter for when stage = WON.

Step 1: Create a Workify API Key

In Workify, go to Settings → API Keys, create a key named "Twenty CRM", and grant invoices:write and clients:read (and optionally clients:write) scopes.

Step 2: Configure a Twenty Webhook

In Twenty's settings (or via the API), create a webhook subscription:

Via Twenty UI: Settings → Webhooks → Add webhook

Via Twenty API (GraphQL):

mutation {
  createWebhook(data: {
    targetUrl: "https://your-worker.your-domain.workers.dev/workify"
    operation: "opportunity.updated"
  }) {
    id
    targetUrl
  }
}

Point targetUrl at your serverless function endpoint.

Step 3: Build the Transform Function

Twenty and Workify speak different schemas, so a small transform is needed. Here's a Cloudflare Worker that handles the conversion:

export default {
  async fetch(request: Request): Promise<Response> {
    const payload = await request.json() as any

    // Only act on opportunity Won events
    if (payload.objectMetadata?.nameSingular !== 'opportunity') return new Response('ok')
    if (payload.record?.stage !== 'WON') return new Response('ok')

    const opp = payload.record

    // Look up the Workify client by email
    const clientRes = await fetch(
      `https://getworkify.app/api/v1/clients?email=${opp.pointOfContact?.email}`,
      { headers: { Authorization: `Bearer ${env.WORKIFY_API_KEY}` } }
    )
    const clients = await clientRes.json() as any
    const clientId = clients.data?.[0]?.id

    if (!clientId) {
      console.error('No Workify client found for', opp.pointOfContact?.email)
      return new Response('client not found', { status: 404 })
    }

    // Create the invoice
    const invoiceRes = await fetch('https://getworkify.app/api/v1/invoices', {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${env.WORKIFY_API_KEY}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        org_id: env.WORKIFY_ORG_ID,
        client_id: clientId,
        currency: 'GBP',
        notes: `Re: ${opp.name}`,
        line_items: [{
          description: opp.name,
          quantity: 1,
          unit_price: (opp.amount?.amountMicros ?? 0) / 1000000,
        }],
        due_date: new Date(Date.now() + 30 * 86400000).toISOString().split('T')[0],
      }),
    })

    const invoice = await invoiceRes.json()
    console.log('Created invoice', invoice)
    return new Response('ok')
  }
}

Set WORKIFY_API_KEY and WORKIFY_ORG_ID as environment variables/secrets in your worker.

Note on amounts: Twenty stores monetary values in amountMicros (millionths of the currency unit). Divide by 1,000,000 to get the major unit value for Workify's unit_price (e.g. 1500000000 amountMicros → 1500 = £1,500).

Step 4: Deploy and Test

Deploy your worker:

wrangler deploy

In Twenty, move a test opportunity to Won and check your worker logs. A Workify invoice should appear as a draft within seconds.

Step 5: Write Back to Twenty

Close the loop by updating the Twenty opportunity with the Workify invoice ID after creation. Add to your worker after the invoice is created:

await fetch(`https://api.twenty.com/objects/opportunities/${opp.id}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${env.TWENTY_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    // Requires a custom field in Twenty — add via Settings → Objects → Opportunities
    workifyInvoiceId: invoice.id,
  }),
})

Using Make or n8n Instead

If you prefer not to write code, use Make or n8n as middleware:

  • Make: Webhooks trigger → HTTP module to Workify API (see our Make guide)
  • n8n: Webhook node → HTTP Request node with the same payload structure

Both work well; the serverless approach is faster and removes a dependency on a third-party platform.

Tips

  • Twenty's metadata API lets you create custom fields programmatically — add a workifyInvoiceId field to Opportunity objects for the write-back
  • If you self-host Twenty, consider co-locating the transform worker in the same region for minimal latency
  • Twenty's webhook system supports HMAC signature verification — validate the x-twenty-webhook-signature header in your worker before processing

Ready to automate your invoicing?

Get your Workify API key and start building in minutes. Pro plan includes full API and webhook access.