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, for example Jira, ServiceNow, Zendesk, Asana and Notion. Replace the target URL, the Authorization header, and the payload_template for 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 API of the tracker.
  • Payload template converts the parsed email into the JSON body that the API expects.

You set both fields in the webhook edit form in the web UI.


Prerequisites

  • A Conduit account. See Getting Started.
  • A domain that is verified in Conduit, or 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. Give it access to one repository only: the repository that receives the issues.
  3. In Repository permissions, set Issues to Read and write.
  4. Click Generate token and copy the github_pat_... value. GitHub shows it one time only.

Linear

  1. Open linear.appSettingsAPIPersonal API keys.
  2. Click Create new key, label it, for example "Conduit email-to-issue", and copy the lin_api_... value.
  3. Write down the team ID of the team for the new issues. The easiest method is this query against the Linear 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 carries the id of every team.

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."
}

The GitHub Create an issue endpoint accepts this body directly. It answers with the URL of the new issue.

Linear

Linear has one GraphQL endpoint, so the template puts the email inside 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_...
    
    A Linear API key needs no 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}}\"}}}"
}

A Linear API key needs no 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 appears in your tracker after 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 becomes a new issue in the tracker."

A new issue appears in the GitHub repository or the Linear team after a few seconds.


Step 4: Review delivery logs

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

Common errors:

Status Cause What to do
401 from GitHub The token has no issues: write permission on the repository. Create the token again with the correct permission.
404 from GitHub The repository path in target_url is wrong, or the token cannot see the repository. Make sure that the URL and the repository permission of the token are correct.
200 from Linear with success: false in the body The teamId is wrong, or the field shape is wrong. Run the team query in Step 1 again. Make sure that the GraphQL response contains the team.
422 from GitHub The rendered payload is not valid JSON. A " or a \ in the email subject is the usual cause. See Escaping caveats.

Escaping caveats

payload_template is a Go text/template. Conduit renders it against the parsed email and puts the result into the request body without a change. Conduit does not JSON-escape the template values.

CAUTION: An email subject such as He said "hi" produces invalid JSON in a "title": "{{.Subject}}" field.

There are two practical answers:

  1. Trust your senders. Keep the address internal, and protect it with an SMTP security policy that requires DKIM from your own domain. Malformed input is then unlikely.
  2. Sanitize in a service of your own. Point target_url at a small handler that you own, for example a Cloud Function or a Worker. The handler serializes the payload safely and then forwards it to GitHub or Linear. It can also add assignees, labels and project boards to the ticket.

Restricting senders

bugs@mail.yourcompany.com is a public address. Without a control, anybody can file an issue in your tracker. Attach an SMTP security policy that requires SPF or DKIM from your own domain. Or limit the source IP addresses to your own egress range. The steps are the same as in the Slack example.

Also set a rate limit on the webhook. An email loop, or a forwarder with wrong settings, then cannot create hundreds of tickets in one minute. Set the Rate limit field in the webhook edit form to the maximum emails per minute.


Next steps