Skip to content

API Reference

guyinwonder168 edited this page Feb 7, 2026 · 2 revisions

API Reference

This guide covers the complete webhook payload structure and response codes.

Table of Contents


Event Payload Structure

All webhook payloads follow this base structure:

{
  "event_id": "string (UUID)",
  "event_type": "issue|time_entry",
  "action": "created|updated|deleted",
  "occurred_at": "ISO 8601 datetime",
  "redmine_url": "string (URL)",
  "actor": {
    "id": "integer",
    "login": "string",
    "name": "string",
    "email": "string (optional)"
  },
  "resource": {
    // Issue or TimeEntry object based on event_type
  }
}

Field Descriptions

Field Type Description Example
event_id UUID string Unique identifier for this event
event_type string Type of resource: "issue" or "time_entry"
action string What happened: "created", "updated", or "deleted"
occurred_at ISO 8601 datetime When the event happened in Redmine
redmine_url string Direct link to the Redmine resource
actor object User who triggered the event
resource object The actual issue or time entry data

Issue Payload Examples

Minimal Mode (Issue Created)

{
  "event_id": "550e8400-e29b-41d4-a716-446614014",
  "event_type": "issue",
  "action": "created",
  "occurred_at": "2026-02-03T12:00:00Z",
  "redmine_url": "https://redmine.example.com/issues/123",
  "actor": {
    "id": 1,
    "login": "admin",
    "name": "Administrator"
  },
  "issue": {
    "id": 123,
    "subject": "Bug fix needed",
    "description": "Fix authentication in login flow",
    "status": { "id": 1, "name": "New" },
    "priority": { "id": 4, "name": "High" },
    "tracker": { "id": 1, "name": "Bug" },
    "project": { "id": 5, "name": "Main Project" },
    "author": { "id": 1, "login": "admin", "name": "Administrator" },
    "is_private": false,
    "created_on": "2026-02-03T12:00:00Z",
    "updated_on": "2026-02-03T12:00:00Z"
  }
}

Standard Mode (Issue Updated)

{
  "event_id": "550e8400-e29b-41d4-a716-446614015",
  "event_type": "issue",
  "action": "updated",
  "occurred_at": "2026-02-03T12:00:00Z",
  "redmine_url": "https://redmine.example.com/issues/123",
  "actor": {
    "id": 2,
    "login": "developer",
    "name": "Developer",
    "email": "developer@example.com"
  },
  "issue": {
    "id": 123,
    "subject": "Bug fix needed",
    "description": "Fix authentication in login flow",
    "status": { "id": 2, "name": "In Progress" },
    "priority": { "id": 4, "name": "High" },
    "tracker": { "id": 1, "name": "Bug" },
    "assigned_to": { "id": 2, "login": "developer", "name": "Developer" },
    "project": { "id": 5, "name": "Main Project" },
    "author": { "id": 1, "login": "admin", "name": "Administrator" },
    "is_private": false,
    "created_on": "2026-02-02T10:00:00Z",
    "updated_on": "2026-02-03T12:00:00Z",
    "last_note": "Looking into this",
    "custom_fields": [
      { "id": 1, "name": "Severity", "value": "Critical" }
    ]
  },
  "changes": {
    "status": { "old": { "id": 1, "name": "New" }, "new": { "id": 2, "name": "In Progress" } },
    "assigned_to": { "old": null, "new": { "id": 2, "login": "developer" } },
    "priority": { "old": { "id": 5, "name": "Normal" }, "new": { "id": 4, "name": "High" }
  }
}

Changes Payload Detail

The changes object (included in Standard/Full mode) contains all modified fields:

"changes": {
  "status": {
    "old": { "id": 1, "name": "New" },
    "new": { "id": 2, "name": "In Progress" }
  },
  "assigned_to": {
    "old": null,
    "new": { "id": 2, "login": "developer" }
  },
  "priority": {
    "old": { "id": 5, "name": "Normal" },
    "new": { "id": 4, "name": "High" }
  },
  "custom_fields": {
    "1": { "old": "Low", "new": "Critical" }
  },
  "estimated_hours": {
    "old": 5.0,
    "new": 8.0
  },
  "done_ratio": {
    "old": 0,
    "new": 0
  },
  "is_private": {
    "old": false,
    "new": true
  },
  "category": {
    "old": { "id": 1, "name": "Development" },
    "new": { "id": 2, "name": "Support" }
  }
}

Time Entry Payload Examples

Minimal Mode (Time Entry Created)

{
  "event_id": "760e6600-f12c-34d5-b678-532610428",
  "event_type": "time_entry",
  "action": "created",
  "occurred_at": "2026-02-03T12:00:00Z",
  "redmine_url": "https://redmine.example.com/time_entries/45",
  "actor": {
    "id": 2,
    "login": "developer",
    "name": "Developer"
  },
  "time_entry": {
    "id": 45,
    "project": {
      "id": 5,
      "name": "Main Project"
    },
    "issue": {
      "id": 123,
      "subject": "Bug fix needed"
    },
    "user": {
      "id": 2,
      "login": "developer",
      "name": "Developer"
    },
    "activity": {
      "id": 9,
      "name": "Development"
    },
    "hours": 2.5,
    "comments": "Fixed authentication bug",
    "spent_on": "2026-02-03",
    "created_on": "2026-02-03T12:00:00Z",
    "updated_on": "2026-02-03T12:00:00Z"
  }
}

Standard Mode (Time Entry Updated)

{
  "event_id": "760e6600-f12c-34d5-b678-532610429",
  "event_type": "time_entry",
  "action": "updated",
  "occurred_at": "2026-02-03T12:00:00Z",
  "redmine_url": "https://redmine.example.com/time_entries/45",
  "actor": {
    "id": 2,
    "login": "developer",
    "name": "Developer"
  },
  "time_entry": {
    "id": 45,
    "project": {
      "id": 5,
      "name": "Main Project"
    },
    "issue": {
      "id": 123,
      "subject": "Bug fix needed"
    },
    "user": {
      "id": 2,
      "login": "developer",
      "name": "Developer"
    },
    "activity": {
      "id": 9,
      "name": "Development"
    },
    "hours": 3.0,
    "comments": "Additional investigation required",
    "spent_on": "2026-02-03",
    "created_on": "2026-02-02T10:00:00Z",
    "updated_on": "2026-02-03T12:00:00Z"
  },
  "changes": {
    "hours": { "old": 2.5, "new": 3.0 },
    "comments": { "old": "Fixed authentication bug", "new": "Additional investigation required" }
  }
}

Payload Comparison

Mode Feature Comparison

Feature Minimal Standard Full
Size Small (~1KB) Medium (~5KB) Large (~20KB)
Issue Fields Core fields only Core + last_note All fields
Time Entry Fields Core fields only Core + comments All fields
Changes Object Not included Included Included
Journal/Notes Not included Last note only Full history
Custom Fields Not included Included Included
Use Case Simple notifications Most integrations Data sync

When to Use Each Mode

Minimal Mode:

  • Slack notifications (simple format)
  • Microsoft Teams (cards only)
  • High-volume systems
  • Testing webhook connectivity
  • Bandwidth-constrained environments

Standard Mode:

  • Most webhook integrations
  • When you need issue context
  • Balancing detail vs. size
  • Audit trails and reporting

Full Mode:

  • Custom integrations requiring all data
  • Data synchronization systems
  • Data warehousing and analytics
  • When bandwidth is not a concern

Response Status Codes

2xx Success Codes

Code Meaning Plugin Action
200 OK Delivery marked "Success"
201 Created Resource created (applicable for some integrations)
202 Accepted Request accepted (applicable for some integrations)
204 No Content Acknowledged without data

4xx Client Error Codes

Code Meaning Plugin Action Common Causes
400 Bad Request Invalid JSON, missing required fields
401 Unauthorized Invalid API key, wrong token
403 Forbidden IP not whitelisted, insufficient permissions
404 Not Found Invalid endpoint URL
429 Too Many Requests Rate limiting exceeded

5xx Server Error Codes

Code Meaning Plugin Action Common Causes
500 Internal Server Error External service error
502 Bad Gateway Upstream server error
503 Service Unavailable External service down or overloaded
504 Gateway Timeout External service timeout

Webhook Signature

HMAC-SHA256 Signature

When HMAC signature is enabled, the payload includes an additional field:

{
  "event_id": "uuid",
  "event_type": "issue",
  "action": "created",
  "occurred_at": "2026-02-03T12:00:00Z",
  "redmine_url": "https://redmine.example.com/issues/123",
  "actor": { "id": 1, "login": "admin" },
  "resource": {
    "id": 123,
    "subject": "Bug fix needed"
  },
  "signature": "a1b2c3d4e5f67898be2dbd5f8f3a4f4c4e9b1c266"
}

Verifying Signature

Python Example:

import hmac
import json
import hashlib

# Shared secret (from external service configuration)
SECRET = "your_secret_key_here"

def verify_webhook(payload_json):
    # Extract signature and payload body
    received_signature = payload_json.get('signature')
    payload_body = json.dumps({k: v for k, v in payload_json.items() if k != 'signature'})
    
    # Calculate expected signature
    expected_signature = hmac.new(
        payload_body.encode('utf-8'),
        SECRET.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    
    # Verify
    if received_signature == expected_signature:
        print("✓ Signature valid")
        return True
    else:
        print("✗ Invalid signature")
        return False

# Usage
payload = {
  "event_id": "550e8400-e29b-41d4-a716-446614014",
  "event_type": "issue",
  "action": "created",
  "occurred_at": "2026-02-03T12:00:00Z",
  "redmine_url": "https://redmine.example.com/issues/123",
  "actor": { "id": 1, "login": "admin" },
  "resource": { "id": 123, "subject": "Bug fix" }
}

if verify_webhook(payload):
    print("Processing webhook...")
else:
    print("Rejecting webhook...")

Ruby Example:

require 'openssl'
require 'json'

SECRET = "your_secret_key_here"

def verify_webhook(payload)
  received_sig = payload['signature']
  
  # Remove signature from payload for calculation
  payload_body = payload.except('signature').to_json
  
  # Calculate HMAC
  expected_sig = OpenSSL::HMAC.hex(
    SECRET,
    payload_body,
    OpenSSL::Digest::SHA256
  )
  
  # Verify
  expected_sig == received_sig
end

# Usage
require 'json'
payload = JSON.parse(request.body)
if verify_webhook(payload)
  # Process webhook
else
  status 401
  body 'Invalid signature'
end

Timestamp for Replay Protection

To prevent replay attacks, the plugin includes event timestamp:

{
  "occurred_at": "2026-02-03T12:00:00Z",
  "timestamp": 1706971200  // Unix timestamp (optional)
}

Verification:

from datetime import datetime, timezone

payload_ts = payload['occurred_at']  # ISO 8601 string
server_time = datetime.now(timezone.utc).isoformat()

# Reject if event is too old (e.g., older than 5 minutes)
if (server_time - payload_ts).total_seconds() > 300:
    print("Event too old - possible replay attack")
    status 401

Error Handling

Error Response Structure

When a webhook delivery fails, the response may include error details:

{
  "status": 404,
  "error": {
    "code": "ENDPOINT_NOT_FOUND",
    "message": "Webhook endpoint not found",
    "details": "The requested URL does not exist"
  },
  "attempt": 3,
  "next_retry_at": "2026-02-03T12:15:00Z"
}

Common Error Codes

Error Code Description Retry Strategy
VALIDATION_ERROR Invalid JSON structure No retry (fix endpoint config)
AUTHENTICATION_FAILED Invalid credentials No retry (fix auth first)
ENDPOINT_NOT_FOUND URL incorrect No retry (fix endpoint URL)
RATE_LIMIT_EXCEEDED Too many requests Retry with delay
TIMEOUT_ERROR External service timeout Retry immediately
SERVER_ERROR 5xx status Retry with backoff

Getting Help

Payload Questions

See detailed payload examples above or:

Integration Issues

Problem Solution Documentation
Unexpected payload format Check endpoint event configuration Configuration
Missing fields Verify payload mode setting Payload Comparison
Authentication failures Check credentials/IP whitelist Security
429 errors Implement rate limiting Security

Resources


Last Updated: 2026-02-03
Plugin Version: 1.0.0-RC1