Example: Create a GitHub Issue or Linear Ticket from Email

This walkthrough shows how to forward emails sent to bugs@mail.yourcompany.com into a GitHub repository or Linear team as new tickets. The email subject becomes the ticket title and the body becomes the description.

The same pattern works for any tracker with a JSON HTTP API: Jira, ServiceNow, Zendesk, Asana, Notion. Replace the target URL, the Authorization header, and the payload_template to match your provider.


Overview

Sender → SMTP → Conduit → GitHub or Linear API → New ticket

Two webhook fields do the work:

  • Custom headers carries the Authorization header to the tracker's API.
  • Payload template reshapes the parsed email into the JSON body the API expects.

Both fields are set in the webhook edit form in the web UI.


Prerequisites

  • A Conduit account. See Getting Started.
  • A domain verified in Conduit, or use of the shared public domain. See Using a Custom Domain.
  • A GitHub personal access token with issues: write on the target repo, or a Linear API key plus your team ID.

Step 1: Get a tracker credential

GitHub

  1. Open github.com → your profile menu → SettingsDeveloper settingsPersonal access tokensFine-grained tokens.
  2. Click Generate new token. Scope it to the single repository that should receive issues.
  3. Under Repository permissions, set Issues to Read and write.
  4. Click Generate token and copy the github_pat_... value. It is shown once.

Linear

  1. Open linear.appSettingsAPIPersonal API keys.
  2. Click Create new key, label it (e.g. "Conduit email-to-issue"), and copy the lin_api_... value.
  3. Note the team ID of the team you want issues created in. The simplest way is to run the following query against Linear's GraphQL API:
    curl -s https://api.linear.app/graphql \
      -H "Authorization: lin_api_..." \
      -H "Content-Type: application/json" \
      -d '{"query": "{ teams { nodes { id name } } }"}'
    
    The response contains every team's id.

Step 2: Create the Conduit webhook

GitHub

Using the web UI

  1. Go to Webhooks (/app/webhooks) and click + New.
  2. Set the address to bugs@mail.yourcompany.com.
  3. Set the target URL to https://api.github.com/repos/yourorg/yourrepo/issues.
  4. Click Create webhook.
  5. On the webhook detail page, click Edit.
  6. In the Custom headers field, enter:
    Authorization: Bearer github_pat_...
    
  7. In the Payload template field, enter:
    {"title": "{{.Subject}}", "body": "From: {{.From}}\n\n{{.Text}}"}
    
  8. Click Save changes.

Using the API

POST /api/v1/webhooks
Authorization: Bearer <conduit_access_token>
Content-Type: application/json

{
  "address": "bugs@mail.yourcompany.com",
  "target_url": "https://api.github.com/repos/yourorg/yourrepo/issues",
  "custom_headers": {
    "Authorization": "Bearer github_pat_..."
  },
  "payload_template": "{\"title\": \"{{.Subject}}\", \"body\": \"From: {{.From}}\\n\\n{{.Text}}\"}"
}

The rendered body for a real email looks like:

{
  "title": "Login button broken on Safari",
  "body": "From: alice@example.com\n\nClicking the login button on the homepage does nothing in Safari 17."
}

GitHub's Create an issue endpoint accepts this directly and responds with the new issue's URL.

Linear

Linear uses a single GraphQL endpoint, so the template wraps the email in a mutation.

Using the web UI

  1. Go to Webhooks (/app/webhooks) and click + New.
  2. Set the address to bugs@mail.yourcompany.com.
  3. Set the target URL to https://api.linear.app/graphql.
  4. Click Create webhook.
  5. On the webhook detail page, click Edit.
  6. In the Custom headers field, enter:
    Authorization: lin_api_...
    
    (Linear API keys are passed without a Bearer prefix.)
  7. In the Payload template field, enter:
    {"query": "mutation IssueCreate($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }", "variables": {"input": {"teamId": "YOUR_TEAM_ID", "title": "{{.Subject}}", "description": "From: {{.From}}\n\n{{.Text}}"}}}
    
  8. Click Save changes.

Using the API

POST /api/v1/webhooks
Authorization: Bearer <conduit_access_token>
Content-Type: application/json

{
  "address": "bugs@mail.yourcompany.com",
  "target_url": "https://api.linear.app/graphql",
  "custom_headers": {
    "Authorization": "lin_api_..."
  },
  "payload_template": "{\"query\": \"mutation IssueCreate($input: IssueCreateInput!) { issueCreate(input: $input) { success issue { identifier url } } }\", \"variables\": {\"input\": {\"teamId\": \"YOUR_TEAM_ID\", \"title\": \"{{.Subject}}\", \"description\": \"From: {{.From}}\\n\\n{{.Text}}\"}}}"
}

Linear API keys are passed without a Bearer prefix (Authorization: lin_api_...).


Step 3: Test the integration

Using the web UI

Open the webhook detail page (/app/webhooks/{id}) and click Simulate. Conduit sends a synthetic payload through the template to GitHub or Linear. A new issue should appear in your tracker within a few seconds.

Or send a real test email

swaks \
  --to bugs@mail.yourcompany.com \
  --from you@example.com \
  --server mx.conduit.email \
  --port 25 \
  --header "Subject: Test ticket from Conduit" \
  --body "This should appear as a new issue in the tracker."

A new issue should appear in the GitHub repo or Linear team within a few seconds.


Step 4: Review delivery logs

Open the webhook's Logs page (/app/webhooks/{id}/logs) to see the status of every delivery attempt. See Delivery Logs for field definitions and filters.

Common errors:

Status Cause Resolution
401 from GitHub Token lacks issues: write on the repo. Recreate the token with the correct scope.
404 from GitHub Repo path in target_url is wrong, or the token cannot see the repo. Confirm the URL and the token's repository scope.
200 from Linear with success: false in the body The teamId is invalid or the field shape is wrong. Re-run the team-listing query in Step 1; check that the GraphQL response contains the team.
422 from GitHub The rendered payload is not valid JSON, often because the email subject contained a " or \. See Escaping caveats below.

Escaping caveats

payload_template is a Go text/template rendered against the parsed email and inserted verbatim into the request body. Conduit does not JSON-escape template values for you, so an email subject like He said "hi" will produce invalid JSON when interpolated into a "title": "{{.Subject}}" field.

Two practical mitigations:

  1. Trust your senders. If the address is internal-only and protected by an SMTP security policy requiring DKIM from your own domain, malformed input is unlikely.
  2. Sanitize at an intermediary. Point target_url at a small handler you own (a Cloud Function, a Worker) that re-serializes the payload safely before forwarding to GitHub or Linear. The handler can also enrich the ticket (assignees, labels, project boards).

Restricting senders

bugs@mail.yourcompany.com is a public address; without controls anyone can file an issue in your tracker. Attach an SMTP security policy that requires SPF or DKIM from your own domain, or restrict source IPs to your own egress range. The mechanics are the same as in the Slack example.

A rate limit on the webhook is also worth setting so that an email loop or a misconfigured forwarder cannot create hundreds of tickets in a minute. Set the Rate limit field in the webhook edit form to the maximum number of emails to accept per minute.


Next steps