📖 Developer Documentation

Complete guide to integrating with skunk.to

🚀 Getting Started

skunk.to provides two ways to integrate with our platform:

  1. OAuth 2.0 - Enable "Login with skunk.to" on your website, allowing users to authenticate using their skunk.to account.
  2. API Keys - Access the API directly to upload files and create pastes programmatically.

To get started, visit the Developer Dashboard to create your OAuth application or API keys.

🔐 OAuth 2.0 / "Login with skunk.to"

Overview

skunk.to implements the OAuth 2.0 Authorization Code flow with support for PKCE (Proof Key for Code Exchange). This allows users to securely authorize your application to access their skunk.to account without sharing their password.

✅ Standards Compliant
Our OAuth implementation follows RFC 6749 (OAuth 2.0) and RFC 7636 (PKCE), making it compatible with any OAuth 2.0 client library.

Setting Up Your Application

  1. Go to the Developer Dashboard
  2. Click "Create Application"
  3. Fill in your application details:
    • Name - The name users will see when authorizing
    • Redirect URIs - URLs where users will be redirected after authorization
    • Application Type:
      • Confidential - For server-side apps that can securely store a client secret
      • Public - For SPAs and mobile apps (uses PKCE instead of client secret)
  4. Save your Client ID and Client Secret (for confidential apps)
⚠️ Important
Your Client Secret is only shown once. Store it securely! If you lose it, you'll need to regenerate it.

Authorization Flow

Step 1: Redirect to Authorization

Direct users to the authorization endpoint:

GET https://skunk.to/oauth/authorize?
    response_type=code
    &client_id=YOUR_CLIENT_ID
    &redirect_uri=https://yourapp.com/callback
    &scope=openid profile:read
    &state=RANDOM_STATE_STRING
ParameterDescription
response_typerequired Must be code
client_idrequired Your application's Client ID
redirect_urirequired Must match one of your registered redirect URIs
scopeoptional Space-separated list of scopes. Defaults to openid
staterecommended Random string to prevent CSRF attacks. Will be returned unchanged.
code_challengePKCE SHA-256 hash of code_verifier (for public clients)
code_challenge_methodPKCE Must be S256

Step 2: User Authorizes

The user will see a consent screen showing your app name and requested permissions. After approval, they're redirected to your redirect_uri with an authorization code:

https://yourapp.com/callback?code=AUTH_CODE&state=RANDOM_STATE_STRING

Step 3: Exchange Code for Tokens

Exchange the authorization code for access and refresh tokens:

POST https://skunk.to/oauth/token
POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=AUTH_CODE
&redirect_uri=https://yourapp.com/callback
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

Response:

{
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "dGhpcyBpcyBhIHJlZnJl...",
    "scope": "openid profile:read"
}

Step 4: Use the Access Token

Include the access token in API requests:

GET https://skunk.to/oauth/userinfo
Authorization: Bearer ACCESS_TOKEN

Step 5: Refresh Tokens

When the access token expires, use the refresh token to get a new one:

POST /oauth/token
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token
&refresh_token=YOUR_REFRESH_TOKEN
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET

PKCE for Public Clients

For SPAs, mobile apps, and other public clients that can't securely store a client secret, use PKCE:

Generate Code Verifier and Challenge

// JavaScript example
function generateCodeVerifier() {
    const array = new Uint8Array(32);
    crypto.getRandomValues(array);
    return btoa(String.fromCharCode(...array))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

async function generateCodeChallenge(verifier) {
    const encoder = new TextEncoder();
    const data = encoder.encode(verifier);
    const digest = await crypto.subtle.digest('SHA-256', data);
    return btoa(String.fromCharCode(...new Uint8Array(digest)))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
}

const codeVerifier = generateCodeVerifier();
const codeChallenge = await generateCodeChallenge(codeVerifier);

// Store codeVerifier securely (e.g., sessionStorage)
// Include codeChallenge in authorization request

Include in authorization request:

&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256

Include in token exchange:

&code_verifier=CODE_VERIFIER

Available Scopes

ScopeDescription
openidVerify user identity (required for login)
profile:readRead basic profile information (username, ID)
profile:writeUpdate profile information
uploads:readView user's uploaded files
uploads:writeUpload files on behalf of user
pastes:readView user's pastes
pastes:writeCreate pastes on behalf of user

OAuth Endpoints

EndpointURL
Authorizationhttps://skunk.to/oauth/authorize
Tokenhttps://skunk.to/oauth/token
User Infohttps://skunk.to/oauth/userinfo
Token Revocationhttps://skunk.to/oauth/revoke

Login Button Integration

Add a "Login with skunk.to" button to your site:

<!-- HTML -->
<a href="https://skunk.to/oauth/authorize?
    response_type=code
    &client_id=YOUR_CLIENT_ID
    &redirect_uri=https://yourapp.com/callback
    &scope=openid profile:read
    &state=RANDOM_STATE"
   class="skunk-login-btn">
    🦨 Login with skunk.to
</a>

<!-- CSS -->
<style>
.skunk-login-btn {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    background: linear-gradient(135deg, #7c3aed, #a855f7);
    color: white;
    padding: 0.75rem 1.5rem;
    border-radius: 8px;
    font-weight: 600;
    text-decoration: none;
    font-family: system-ui, sans-serif;
}
.skunk-login-btn:hover { opacity: 0.9; }
</style>

🔑 API Keys

API keys provide direct access to the skunk.to API without requiring user authorization. They're ideal for:

  • Automated scripts and tools
  • Server-side integrations
  • ShareX configuration
  • CI/CD pipelines

Using API Keys

Include your API key in the Authorization header:

Authorization: Bearer sk_live_xxxx_xxxxxxxxxxxxxxxx

Or as a query parameter (not recommended for security):

GET https://skunk.to/api/upload?api_key=sk_live_xxxx_xxxxxxxxxxxxxxxx

API Key Scopes

When creating an API key, you can limit its permissions to specific scopes. This follows the principle of least privilege.

ScopeAllows
uploads:readList and view your uploads
uploads:writeUpload new files, delete uploads
pastes:readList and view your pastes
pastes:writeCreate new pastes, delete pastes
profile:readView account information

📡 API Reference

User Info

GET /oauth/userinfo

Returns information about the authenticated user.

Required Scope: openid or profile:read

{
    "sub": "user_abc123",
    "name": "johndoe",
    "preferred_username": "johndoe",
    "picture": null,
    "updated_at": 1704067200
}

File Uploads

POST /api/upload

Upload a file.

Required Scope: uploads:write

curl -X POST "https://skunk.to/api/upload" \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -F "file=@/path/to/file.png"

Response:

{
    "success": true,
    "id": "abc123",
    "url": "https://skunk.to/abc123",
    "raw_url": "https://skunk.to/abc123/raw",
    "deletion_key": "del_xyz789",
    "deletion_url": "https://skunk.to/abc123?delete=del_xyz789"
}

Pastes

POST /api/paste

Create a new paste.

Required Scope: pastes:write

curl -X POST "https://skunk.to/api/paste" \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
        "content": "console.log(\"Hello World\");",
        "syntax": "javascript",
        "title": "My Code Snippet"
    }'

Response:

{
    "success": true,
    "id": "paste123",
    "url": "https://skunk.to/p/paste123",
    "raw_url": "https://skunk.to/p/paste123/raw",
    "deletion_key": "del_abc456",
    "deletion_url": "https://skunk.to/p/paste123?delete=del_abc456"
}

💻 Code Examples

Python - OAuth Client

import requests
from urllib.parse import urlencode

CLIENT_ID = "sk_your_client_id"
CLIENT_SECRET = "sks_your_client_secret"
REDIRECT_URI = "https://yourapp.com/callback"

# Step 1: Generate authorization URL
auth_params = {
    "response_type": "code",
    "client_id": CLIENT_ID,
    "redirect_uri": REDIRECT_URI,
    "scope": "openid profile:read",
    "state": "random_state_string"
}
auth_url = f"https://skunk.to/oauth/authorize?{urlencode(auth_params)}"
print(f"Redirect user to: {auth_url}")

# Step 2: Exchange code for tokens (in your callback handler)
def exchange_code(code):
    response = requests.post("https://skunk.to/oauth/token", data={
        "grant_type": "authorization_code",
        "code": code,
        "redirect_uri": REDIRECT_URI,
        "client_id": CLIENT_ID,
        "client_secret": CLIENT_SECRET
    })
    return response.json()

# Step 3: Get user info
def get_user_info(access_token):
    response = requests.get("https://skunk.to/oauth/userinfo", headers={
        "Authorization": f"Bearer {access_token}"
    })
    return response.json()

JavaScript - File Upload with API Key

const API_KEY = 'sk_live_xxxx_xxxxxxxxxxxxxxxx';

async function uploadFile(file) {
    const formData = new FormData();
    formData.append('file', file);

    const response = await fetch('https://skunk.to/api/upload', {
        method: 'POST',
        headers: {
            'Authorization': `Bearer ${API_KEY}`
        },
        body: formData
    });

    return response.json();
}

// Usage
const fileInput = document.querySelector('input[type="file"]');
fileInput.addEventListener('change', async (e) => {
    const result = await uploadFile(e.target.files[0]);
    console.log('Uploaded:', result.url);
});

cURL - Create Paste

curl -X POST "https://skunk.to/api/paste" \
    -H "Authorization: Bearer sk_live_xxxx_xxxxxxxxxxxxxxxx" \
    -H "Content-Type: application/json" \
    -d '{
        "content": "Hello, World!",
        "syntax": "plaintext",
        "title": "My First Paste"
    }'

⚠️ Error Handling

OAuth Errors

OAuth errors follow RFC 6749 format:

{
    "error": "invalid_grant",
    "error_description": "Authorization code has expired"
}
Error CodeDescription
invalid_requestMissing or invalid parameters
invalid_clientUnknown client or invalid credentials
invalid_grantInvalid, expired, or revoked authorization code
unauthorized_clientClient not authorized for this grant type
unsupported_grant_typeGrant type not supported
invalid_scopeRequested scope is invalid or unknown
access_deniedUser denied authorization

API Errors

{
    "success": false,
    "error": "Invalid API key"
}

HTTP Status Codes

StatusMeaning
200Success
400Bad request - check parameters
401Unauthorized - invalid or expired token
403Forbidden - insufficient permissions
404Resource not found
429Rate limited - slow down
500Server error
Need Help?
If you have questions or run into issues, check our Developer Dashboard or contact support.