API Documentation
gitmd2pdf provides a RESTful API for programmatic markdown-to-PDF conversion. The API supports two authentication methods: HMAC-SHA256 for direct request signing, and OAuth 2.0 client credentials for token-based access. Both support asynchronous job processing with optional webhooks.
Authentication
You'll need a Premium subscription to access the API. Two authentication methods are available:
| Method | Best For | Token Management |
|---|---|---|
| HMAC | Server-to-server, CI/CD, scripts | No tokens - sign each request |
| OAuth | Long-running sessions, SDKs | Obtain token, use for 1 hour |
Getting API Credentials
- Log in to your gitmd2pdf account
- Navigate to Account > API Keys
- Click Generate New Credentials
- Select credential type: HMAC or OAuth
- Save your
client_idandclient_secretsecurely (secret shown only once)
Auth-by-endpoint summary
The server accepts different auth schemes on different endpoint families. Use this table when wiring an integration — the Authentication: line on each endpoint below repeats the same information:
| Endpoint(s) | Accepted auth schemes |
|---|---|
POST /v1/oauth/token |
Public (no auth header) — exchange client_id + client_secret for a 1-hour OAuth bearer token |
POST /v1/convert |
HMAC or OAuth Bearer |
GET /v1/jobs/:jobId/status |
HMAC or OAuth Bearer |
GET /v1/jobs/:jobId/download |
HMAC or OAuth Bearer |
POST / GET / DELETE /v1/callback-allowlist[/...] |
HMAC or OAuth Bearer (for API customers) — server also accepts Session JWT here, but that's an internal SPA path |
POST / GET / DELETE /v1/credentials[/...] |
Session JWT only (must use the Dashboard / Convert to PDF UI — these endpoints mint and revoke API credentials and are not callable via API auth by design) |
For API customers, the auth model is simple: use HMAC or OAuth Bearer on every endpoint that's not /v1/credentials. The "Session JWT" entries in the table above are an internal SPA implementation detail and not part of the API surface you'll integrate against.
"Session JWT" is the token the browser holds after logging into the SPA at gitmd2pdf.com; it carries {userId, isAdmin, …} and is signed with JWT_SECRET. It is completely separate from OAuth bearer tokens (which are opaque random tokens issued by /v1/oauth/token). The server distinguishes the two by token shape: JWTs have three dot-separated segments; OAuth tokens do not. Two endpoint families pass session JWTs through to their handlers so the SPA can call them on behalf of the logged-in user: /v1/callback-allowlist (the Convert to PDF page's "Callback domains" UI panel) and /v1/credentials (the API Keys management UI). For programmatic API integration neither of these paths is relevant — use HMAC or OAuth Bearer.
Conversion endpoints (/v1/convert, /v1/jobs/:jobId/status, /v1/jobs/:jobId/download) do NOT accept session JWTs at all — HMAC or OAuth Bearer only, regardless of caller.
HMAC Authentication
HMAC authentication signs each request directly using your client secret. No token exchange required.
Request Headers
| Header | Description |
|---|---|
Authorization |
HMAC <client_id>:<signature> |
X-Timestamp |
Unix timestamp in milliseconds |
Computing the Signature
The signature is computed as follows:
signing_string = METHOD + PATH + TIMESTAMP + BODY
secret_hash = SHA256(client_secret)
signature = HMAC-SHA256(secret_hash, signing_string)
Example (JavaScript/Node.js):
const crypto = require('crypto');
function signRequest({ clientId, clientSecret, method, path, body, timestamp }) {
// Hash the client secret
const secretHash = crypto.createHash('sha256')
.update(clientSecret)
.digest('hex');
// Build signing string: METHOD + PATH + TIMESTAMP + BODY
const signingString = `${method}${path}${timestamp}${body}`;
// Compute HMAC-SHA256 signature
const signature = crypto.createHmac('sha256', secretHash)
.update(signingString)
.digest('hex');
return `HMAC ${clientId}:${signature}`;
}
// Usage
const timestamp = String(Date.now());
const body = JSON.stringify({ type: 'repo', repoUrl: 'https://github.com/user/repo' });
const authorization = signRequest({
clientId: 'your_client_id',
clientSecret: 'your_client_secret',
method: 'POST',
path: '/api/v1/convert',
body,
timestamp
});
// Make request
fetch('https://gitmd2pdf.com/api/v1/convert', {
method: 'POST',
headers: {
'Authorization': authorization,
'X-Timestamp': timestamp,
'Content-Type': 'application/json'
},
body
});
Example (Python):
import hashlib
import hmac
import time
import json
import requests
def sign_request(client_id, client_secret, method, path, body, timestamp):
# Hash the client secret
secret_hash = hashlib.sha256(client_secret.encode()).hexdigest()
# Build signing string
signing_string = f"{method}{path}{timestamp}{body}"
# Compute HMAC-SHA256 signature
signature = hmac.new(
secret_hash.encode(),
signing_string.encode(),
hashlib.sha256
).hexdigest()
return f"HMAC {client_id}:{signature}"
# Usage
timestamp = str(int(time.time() * 1000))
body = json.dumps({"type": "repo", "repoUrl": "https://github.com/user/repo"})
authorization = sign_request(
client_id="your_client_id",
client_secret="your_client_secret",
method="POST",
path="/api/v1/convert",
body=body,
timestamp=timestamp
)
response = requests.post(
"https://gitmd2pdf.com/api/v1/convert",
headers={
"Authorization": authorization,
"X-Timestamp": timestamp,
"Content-Type": "application/json"
},
data=body
)
Example (Bash/curl):
#!/bin/bash
CLIENT_ID="your_client_id"
CLIENT_SECRET="your_client_secret"
TIMESTAMP=$(date +%s000)
METHOD="POST"
PATH="/api/v1/convert"
BODY='{"type":"repo","repoUrl":"https://github.com/user/repo"}'
# Hash the secret
SECRET_HASH=$(echo -n "$CLIENT_SECRET" | sha256sum | cut -d' ' -f1)
# Build signing string and compute signature
SIGNING_STRING="${METHOD}${PATH}${TIMESTAMP}${BODY}"
SIGNATURE=$(echo -n "$SIGNING_STRING" | openssl dgst -sha256 -hmac "$SECRET_HASH" | cut -d' ' -f2)
curl -X POST "https://gitmd2pdf.com${PATH}" \
-H "Authorization: HMAC ${CLIENT_ID}:${SIGNATURE}" \
-H "X-Timestamp: ${TIMESTAMP}" \
-H "Content-Type: application/json" \
-d "$BODY"
Replay Protection
Timestamps must be within 5 minutes of server time. Requests with stale or future timestamps will be rejected.
OAuth Authentication
OAuth 2.0 client credentials flow for obtaining bearer tokens.
Obtaining an Access Token
curl -X POST https://gitmd2pdf.com/api/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "your_client_id",
"client_secret": "your_client_secret"
}'
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600
}
Use the access token in subsequent requests:
Authorization: Bearer <access_token>
Tokens expire after 1 hour. Request a new token when expired.
Endpoints
POST /api/v1/convert
Enqueue a markdown-to-PDF conversion job.
Authentication: Required — HMAC or OAuth Bearer. Session JWTs are not accepted on this endpoint. See the Auth-by-endpoint summary above.
Content-Type: application/json or multipart/form-data (for file uploads)
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
type |
string | Yes | Conversion type: file, url, or repo |
url |
string | For url type |
URL to a markdown file |
repoUrl |
string | For repo type |
Git repository URL |
branch |
string | No | Branch name (default: main/master) |
title |
string | No | Custom title for the PDF (max 256 chars) |
callbackUrl |
string | No | Webhook URL for job completion notification |
callbackAuthHeader |
string | No | Authorization header for callback requests |
repoCredentials |
object | No | Per-request HTTPS git credentials for type=repo (private repositories). Object with username (non-empty string) and token (non-empty string, max 512 characters). Single-use — never stored, never logged. Ignored for non-repo types. |
Example: Convert a File
curl -X POST https://gitmd2pdf.com/api/v1/convert \
-H "Authorization: Bearer <access_token>" \
-F "type=file" \
-F "file=@README.md" \
-F "title=My Documentation"
Example: Convert a Repository
curl -X POST https://gitmd2pdf.com/api/v1/convert \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"type": "repo",
"repoUrl": "https://github.com/user/repo",
"branch": "main",
"title": "Project Documentation"
}'
Example: Convert a URL
curl -X POST https://gitmd2pdf.com/api/v1/convert \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"type": "url",
"url": "https://raw.githubusercontent.com/user/repo/main/README.md"
}'
Example: Convert a Private Repository
Pass a single-use HTTPS credential pair via repoCredentials. The token is forwarded only to the git clone step (via subprocess environment) and is never persisted or logged.
curl -X POST https://gitmd2pdf.com/api/v1/convert \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"type": "repo",
"repoUrl": "https://github.com/your-org/private-repo",
"repoCredentials": {
"username": "your-github-username",
"token": "ghp_xxxxxxxxxxxxxxxxxxxx"
}
}'
Response (202 Accepted):
{
"jobId": "job_abc123def456",
"status": "queued"
}
When callbackUrl is provided, the response also includes a callbackSigningSecret you must keep to verify webhook signatures. The secret is returned exactly once — at job creation time — and is not retrievable afterwards.
{
"jobId": "job_abc123def456",
"status": "queued",
"callbackSigningSecret": "cbk_secret_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
GET /api/v1/jobs/:jobId/status
Check the status of a conversion job.
Authentication: Required — HMAC or OAuth Bearer. Session JWTs are not accepted.
Response:
{
"jobId": "job_abc123def456",
"status": "completed",
"createdAt": "2024-01-15T10:30:00Z",
"updatedAt": "2024-01-15T10:30:15Z",
"downloadUrl": "/api/v1/jobs/job_abc123def456/download"
}
Status Values:
queued- Job is waiting to be processedprocessing- Job is currently being convertedcompleted- Conversion successful, PDF ready for downloadfailed- Conversion failed (seeerrorfield)
Response Fields:
| Field | Type | When Present | Description |
|---|---|---|---|
jobId |
string | always | The job identifier |
status |
string | always | One of queued, processing, completed, failed |
createdAt |
ISO 8601 | always | Job enqueue timestamp |
updatedAt |
ISO 8601 | always | Last status-change timestamp (equals completion time when status=completed) |
downloadUrl |
string | status=completed and no callback was used |
Relative URL to download the PDF (one-shot) |
error |
string | status=failed |
Failure reason |
message |
string | status=completed and PDF was already delivered to a callback URL |
Human-readable note that the file is no longer available for download |
callbackRequested |
bool | callback URL was provided on the original request | Always true in that case |
callbackDelivered |
bool | callback URL was provided | Whether the callback POST succeeded |
callbackDeliveredAt |
ISO 8601 | callbackDelivered=true |
Successful-delivery timestamp |
callbackLastAttemptAt |
ISO 8601 | a delivery attempt has occurred | Timestamp of the most recent attempt (success or failure) |
callbackError |
string | callback delivery failed after retries | Last error message |
config_applied |
bool | type=repo job reached status=completed (jobs from change-290 onward) |
true if .gitmd2pdf.yml was found at repo root AND the caller was Premium |
structure_applied |
bool | as above | true if config_applied is true AND the config's structure: key produced a non-empty node list |
GET /api/v1/jobs/:jobId/download
Download the converted PDF file.
Authentication: Required — HMAC or OAuth Bearer. Session JWTs are not accepted.
Response: Binary PDF file with Content-Type: application/pdf
curl -X GET https://gitmd2pdf.com/api/v1/jobs/job_abc123def456/download \
-H "Authorization: Bearer <access_token>" \
-o output.pdf
Webhooks
For asynchronous workflows, configure a callback URL to receive job completion notifications.
Setting Up Webhooks
- Add callback domain to allowlist:
curl -X POST https://gitmd2pdf.com/api/v1/callback-allowlist \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"domain": "api.yourapp.com"}'
- Include callback URL in conversion request:
curl -X POST https://gitmd2pdf.com/api/v1/convert \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{
"type": "repo",
"repoUrl": "https://github.com/user/repo",
"callbackUrl": "https://api.yourapp.com/webhooks/gitmd2pdf",
"callbackAuthHeader": "Bearer your-webhook-secret"
}'
Callback Authentication (callbackAuthHeader) — Bring-Your-Own Credential
gitmd2pdf does not issue, validate, store, or rotate the callback auth credential. It is pure pass-through: the string you supply as callbackAuthHeader is relayed verbatim as the X-Auth header (mirrored as x-apikey) on the inbound callback POST to your receiver. There is no "get the token" step on the gitmd2pdf side — you invent the credential, register it on your own receiver, and tell gitmd2pdf to send it back.
End-to-end flow for a generic customer running their own callback receiver:
- Stand up an HTTPS endpoint on your own infrastructure (e.g.
https://acme.com/pdf-intake). - Implement whatever authentication scheme you want on it — a static bearer token you generate, an HMAC scheme, an API gateway key, mTLS client cert, etc. gitmd2pdf never sees or evaluates this credential.
- Allowlist your domain (gitmd2pdf.com → Convert to PDF → Callback domains → add
acme.com, orPOST /api/v1/callback-allowlist). Exact-hostname match, no wildcards. - When calling
POST /api/v1/convert, include your credential ascallbackAuthHeader: "<whatever-you-chose>"(max 512 chars). - gitmd2pdf relays that string verbatim as the
X-Authheader on the inbound callback POST. Your receiver validates it however it likes.
The callbackAuthHeader field exists precisely so gitmd2pdf does not have to mint and exchange credentials on your behalf — it is a string-relay for callback auth, nothing more.
callbackAuthHeader vs callbackSigningSecret — don't confuse the two:
| Field | Who mints it | Purpose |
|---|---|---|
callbackAuthHeader |
You (any string, any scheme) | Authenticates gitmd2pdf to your receiver via the X-Auth header |
callbackSigningSecret |
gitmd2pdf (returned once at job creation) | Authenticates the PDF body itself via HMAC-SHA256 in X-Signature so you can detect tampering |
Webhook Delivery
When the job completes, gitmd2pdf POSTs the PDF binary directly to your callbackUrl (the body is the PDF bytes — there is no JSON envelope, and no separate /download step is needed).
Request line and headers:
POST <your callbackUrl>
Content-Type: application/pdf
Content-Length: <byte length of PDF>
X-Job-ID: job_abc123def456
X-Delivery-Attempt: 1
X-Signature: sha256=<hex-hmac of body, keyed by callbackSigningSecret>
X-Auth: <your callbackAuthHeader, if provided>
x-apikey: <same value as X-Auth — sent for receivers such as RestDB that expect the key in `x-apikey`>
Body: the PDF file bytes.
Verification. Compute HMAC-SHA256(callbackSigningSecret, body) and compare to the value after sha256= in the X-Signature header. The callbackSigningSecret is returned exactly once at job creation.
Retry policy. Up to 3 attempts (0s, 1s, 2s backoff) with a 30s per-attempt timeout. After successful delivery the PDF is deleted server-side — there is no download fallback for callback-mode jobs.
To inspect delivery status (success, retry count, error), poll GET /api/v1/jobs/:jobId/status; the response includes callbackDelivered, callbackDeliveredAt, callbackLastAttemptAt, and callbackError fields.
Managing Callback Allowlist
Authentication: Required — HMAC or OAuth Bearer, same as every other API endpoint. Caller must be on a Premium account (admins bypass the Premium check).
Implementation note: the server's sessionOrApiAuth middleware on these endpoints also accepts the Dashboard session JWT, which is what the Convert to PDF page in the SPA uses internally when a logged-in user manages their allowlist via the UI panel. API customers do not need to think about this — use HMAC or OAuth Bearer.
Add a domain:
POST /api/v1/callback-allowlist
Content-Type: application/json
{ "domain": "api.yourapp.com" }
List allowed domains:
GET /api/v1/callback-allowlist
Remove a domain:
DELETE /api/v1/callback-allowlist/:domain
API Credentials Management
These endpoints mint, list, and revoke the HMAC / OAuth credentials you use to call every other API endpoint. They are Session JWT only by design — they are not callable via HMAC or OAuth Bearer. The intended path is via the Dashboard / Convert to PDF UI (which is what calls these endpoints behind the scenes). If you want to automate credential rotation, you would do so through a session-authenticated browser flow, not through the API itself.
POST /api/v1/credentials
Generate new API credentials.
Authentication: Required — Session JWT only.
GET /api/v1/credentials
List your API credentials.
Authentication: Required — Session JWT only.
DELETE /api/v1/credentials/:clientId
Revoke API credentials.
Authentication: Required — Session JWT only.
Rate Limits
API requests are throttled per credential using a token-bucket. The bucket has a capacity of 10 tokens and refills over 60 seconds, so sustained usage is bounded at roughly 10 requests per minute with short bursts up to 10.
When the bucket is empty, the server returns 429 Too Many Requests with a Retry-After header (seconds to wait):
HTTP/1.1 429 Too Many Requests
Retry-After: 6
Content-Type: application/json
{ "error": "Rate limit exceeded. Try again later." }
X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset headers are not currently emitted — clients should rely on Retry-After on 429 responses.
Error Responses
All errors follow a consistent format:
{
"error": "Error message describing the issue"
}
| Status Code | Description |
|---|---|
| 400 | Bad Request - Invalid parameters |
| 401 | Unauthorized - Invalid or expired token |
| 403 | Forbidden - Insufficient permissions or plan |
| 404 | Not Found - Resource doesn't exist |
| 429 | Too Many Requests - Rate limit exceeded |
| 500 | Internal Server Error |
| 503 | Service Unavailable - Try again later |
OpenAPI Specification
The complete OpenAPI 3.0 specification is available at:
GET /api/v1/openapi.json
You can import this into tools like Postman, Insomnia, or Swagger UI for interactive API exploration.
Code Examples
Python
import requests
# Get access token
auth_response = requests.post(
"https://gitmd2pdf.com/api/v1/oauth/token",
json={
"grant_type": "client_credentials",
"client_id": "your_client_id",
"client_secret": "your_client_secret"
}
)
token = auth_response.json()["access_token"]
# Convert a repository
headers = {"Authorization": f"Bearer {token}"}
convert_response = requests.post(
"https://gitmd2pdf.com/api/v1/convert",
headers=headers,
json={
"type": "repo",
"repoUrl": "https://github.com/user/repo"
}
)
job_id = convert_response.json()["jobId"]
# Poll for completion
import time
while True:
status = requests.get(
f"https://gitmd2pdf.com/api/v1/jobs/{job_id}/status",
headers=headers
).json()
if status["status"] == "completed":
break
time.sleep(2)
# Download PDF
pdf = requests.get(
f"https://gitmd2pdf.com/api/v1/jobs/{job_id}/download",
headers=headers
)
with open("output.pdf", "wb") as f:
f.write(pdf.content)
Node.js
const axios = require('axios');
const fs = require('fs');
async function convertRepo(repoUrl) {
// Get access token
const { data: auth } = await axios.post(
'https://gitmd2pdf.com/api/v1/oauth/token',
{
grant_type: 'client_credentials',
client_id: process.env.GITMD2PDF_CLIENT_ID,
client_secret: process.env.GITMD2PDF_CLIENT_SECRET
}
);
const headers = { Authorization: `Bearer ${auth.access_token}` };
// Start conversion
const { data: job } = await axios.post(
'https://gitmd2pdf.com/api/v1/convert',
{ type: 'repo', repoUrl },
{ headers }
);
// Poll for completion
let status;
do {
await new Promise(r => setTimeout(r, 2000));
const { data } = await axios.get(
`https://gitmd2pdf.com/api/v1/jobs/${job.jobId}/status`,
{ headers }
);
status = data.status;
} while (status !== 'completed' && status !== 'failed');
// Download PDF
const pdf = await axios.get(
`https://gitmd2pdf.com/api/v1/jobs/${job.jobId}/download`,
{ headers, responseType: 'arraybuffer' }
);
fs.writeFileSync('output.pdf', pdf.data);
}
convertRepo('https://github.com/user/repo');
Support
For API support, contact us at support@mods-software.com or visit our FAQ.