===========================
Kawitan__sk_live_8pn0Mlp4lwO4sLta16K0fVHX2Q0WVQFy__Wekasan
===========================
===========================
Produk Link = IPS Tauhidul Afkar ||| https://s.id/ips-duridwangurunatafkar
===========================
Dokumentasi:
# s.id Developer Platform — API Keys, Webhooks & OAuth
> **Connect your apps with s.id.** This guide explains how to use the s.id public
> developer platform: authenticating with API keys, calling the REST API, receiving
> webhooks, and authorizing third-party apps with OAuth 2.0.
>
> Manage your credentials in the dashboard at **Dashboard → Developer**.
>
> **Interactive API reference (Swagger UI):** `https://dash.s.id/developer/api-docs`
> **OpenAPI spec:** `https://dash.s.id/openapi/sid-developer-api.json`
## Table of Contents
1. [Quick Start](#quick-start)
2. [API Keys](#api-keys)
3. [Public REST API](#public-rest-api)
4. [Webhooks](#webhooks)
5. [Verifying Webhook Signatures](#verifying-webhook-signatures)
6. [OAuth 2.0](#oauth-20)
7. [Rate Limits & Errors](#rate-limits--errors)
8. [Legacy API (v1)](#legacy-api-v1)
---
## Quick Start
1. Open **Dashboard → Developer → API Keys** and click **Create API Key**.
2. Pick the scopes your integration needs (e.g. `links:read`, `links:write`).
3. Copy the key — it starts with `sk_live_` and is shown **only once**.
4. Call the API with the key in the `Authorization` header:
```bash
curl https://api.s.id/v2/links \
-H "Authorization: Bearer sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```
**Base URL:** `https://api.s.id/v2`
---
## API Keys
API keys authenticate server-to-server calls to the public REST API.
| Property | Detail |
|----------|--------|
| Format | `sk_live_` + 32 random base62 chars |
| Header | `Authorization: Bearer sk_live_...` |
| Storage | Hashed (SHA-256) at rest — s.id never stores the raw key |
| Visibility | Shown once on creation; revoke + recreate if lost |
| Scopes | Per-key; a request to an endpoint without the required scope returns `403` |
### Scopes
| Scope | Grants | Status |
|-------|--------|--------|
| `links:read` | List and read links | Available |
| `links:write` | Create and update links (create, edit, restore) | Available |
| `links:archive` | Archive links | Available |
| `links:analytics` | Read per-link and account-level click statistics | Available |
| `qr:read` | Read QR codes for links | Available |
| `qr:write` | Update QR code customization for links | Available |
| `user:read` | Read the authenticated user profile and account quota | Available |
| `microsites:read` | List and read microsites | Available |
| `microsites:write` | Create, update and manage components of microsites | Available |
| `microsites:delete` | Delete microsites and their components | Available |
### Managing keys
Keys are managed via your logged-in session (not with an API key):
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/api/v1/keys` | List your keys |
| `POST` | `/api/v1/keys` | Create a key (`{ "name", "scopes": [] }`) |
| `PATCH` | `/api/v1/keys/:id` | Rename / change scopes |
| `DELETE` | `/api/v1/keys/:id` | Revoke a key |
---
## Public REST API
All routes below accept `Authorization: Bearer sk_live_...` (an API key) **or** an OAuth 2.0 access
token, and require the listed scope.
**Rate limit:** 38 requests/minute per credential (per `sk_live_*` key or per OAuth token) by
default; OAuth apps also have a per-app ceiling. See [Rate Limits & Errors](#rate-limits--errors).
| Method | Path | Scope | Description |
|--------|------|-------|-------------|
| `GET` | `/v2/links` | `links:read` | List links (`?page=&limit=&search=`) |
| `POST` | `/v2/links` | `links:write` | Create a short link |
| `POST` | `/v2/links/bulk` | `links:write` | Create up to 100 links in one request |
| `POST` | `/v2/links/available` | `links:read` | Check if a custom slug is free |
| `GET` | `/v2/links/:short` | `links:read` | Get one link by short code |
| `PUT` | `/v2/links/:short` | `links:write` | Update a link |
| `DELETE` | `/v2/links/:short` | `links:archive` | Archive a link |
| `POST` | `/v2/links/:short/unarchive` | `links:write` | Restore an archived link |
| `GET` | `/v2/links/:short/stats` | `links:analytics` | Per-link click statistics timeline |
| `GET` | `/v2/links/:short/stats/lifetime` | `links:analytics` | Per-link lifetime click count |
| `GET` | `/v2/stats` | `links:analytics` | Account-level click analytics (timeline + breakdowns) |
| `GET` | `/v2/links/:short/qr` | `qr:read` | QR code for a link |
| `PUT` | `/v2/links/:short/qr` | `qr:write` | Update QR customization (logo + color) for a link |
| `GET` | `/v2/qr` | `qr:read` | Account-level default QR code |
| `PUT` | `/v2/qr` | `qr:write` | Update the account-level default QR code |
| `GET` | `/v2/microsites` | `microsites:read` | List your microsites |
| `POST` | `/v2/microsites` | `microsites:write` | Create a microsite |
| `POST` | `/v2/microsites/available` | `microsites:read` | Check if a microsite slug is free |
| `GET` | `/v2/microsites/:slug` | `microsites:read` | Get one microsite by slug |
| `PUT` | `/v2/microsites/:slug` | `microsites:write` | Update a microsite (title, description, visibility) |
| `DELETE` | `/v2/microsites/:slug` | `microsites:delete` | Delete a microsite |
| `GET` | `/v2/microsites/:slug/components` | `microsites:read` | List a microsite's components |
| `POST` | `/v2/microsites/:slug/components` | `microsites:write` | Add a component to a microsite |
| `POST` | `/v2/microsites/:slug/components/order` | `microsites:write` | Reorder a microsite's components |
| `PUT` | `/v2/microsites/:slug/components/:componentId` | `microsites:write` | Update a microsite component |
| `DELETE` | `/v2/microsites/:slug/components/:componentId` | `microsites:delete` | Delete a microsite component |
| `GET` | `/v2/user` | `user:read` | Authenticated user profile |
| `GET` | `/v2/quota` | `user:read` | Account quota and usage |
### Check slug availability
```bash
curl -X POST https://api.s.id/v2/links/available \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{"short": "myslug"}'
```
> **Note:** the request body field is `short`, not `slug`.
Response:
```json
{ "data": { "available": true } }
```
### Create a link
```bash
curl -X POST https://api.s.id/v2/links \
-H "Authorization: Bearer sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"long_url": "https://drcontoh.co.id/a-url-sangat-panjang",
"title": "My Link",
"custom_slug": "drlinkesdotid"
}'
```
Response:
```json
{
"data": {
"id": 123,
"short": "drlinkesdotid",
"short_url": "https://s.id/drlinkesdotid",
"long_url": "https://drcontoh.co.id/a-url-sangat-panjang",
"microsite": false,
"protected": false,
"disposable": false,
"adult": false
},
"message": "link_created"
}
```
### Update a link
`PUT /v2/links/:short` accepts any combination of these optional fields:
| Field | Type | Description |
|-------|------|-------------|
| `long_url` | string | New destination URL (max 2083 chars) |
| `title` | string | Display title (max 48 chars) |
| `password` | string | Set or clear a link password (max 255 chars) |
| `disposable` | bool | Mark as disposable (single-use) |
| `adult` | bool | Mark as adult content |
| `custom_slug` | string | New short code (max 48 chars) |
| `archive_at` | string | ISO 8601 datetime to auto-archive |
| `domain_alias` | string | Custom domain alias |
---
## Webhooks
Webhooks push real-time events to your server. Register an endpoint in
**Dashboard → Developer → Webhooks** (or via the API), choose the events you care
about, and s.id will `POST` a JSON payload to your URL when they happen.
### Events
| Event | Fires when | Status |
|-------|-----------|--------|
| `link.created` | A link is created (UI or API) | Available |
| `link.updated` | A link's destination/title/slug changes | Available |
| `link.archived` | A link is archived (deleted) | Available |
| `link.clicked` | A link is visited | Available |
| `microsite.published` | A microsite is created or made public (hidden → visible) | Available |
| `qr.scanned` | A link is visited from its QR code (`s=qr` marker detected) | Available |
### Delivery
- **Method:** `POST` with `Content-Type: application/json`
- **Retries:** up to 3 attempts per event with back-off (0 s, 5 s, 30 s)
- **Auto-disable:** after 5 consecutive failed *events*, the webhook is disabled
(a single transient failure won't disable it)
- Re-enable by recreating the webhook once your endpoint is healthy
### Headers
| Header | Description |
|--------|-------------|
| `X-SID-Event` | The event name, e.g. `link.created` |
| `X-SID-Signature` | `sha256=<hmac-hex>` — see below |
| `X-SID-Delivery-ID` | Unique delivery identifier |
| `User-Agent` | `s.id-Webhooks/1.0` |
### Example payload
```json
{
"event": "link.created",
"link": {
"id": 123,
"short": "drlinkesdotid",
"short_url": "https://s.id/drlinkesdotid",
"long_url": "https://drcontoh.co.id/a-url-sangat-panjang",
"title": "My Link",
"created": "2026-06-22T10:00:00Z"
},
"timestamp": "2026-06-22T10:00:00Z"
}
```
### Testing & history
- **Test delivery:** the **Test** button (or `POST /api/v1/webhooks/:id/test`) sends a
sample payload and shows the HTTP status your endpoint returned.
- **Delivery history:** the **Deliveries** drawer (or `GET /api/v1/webhooks/:id/deliveries`)
lists recent attempts with status code, error and timestamp.
> **Note:** webhooks only fire for events that actually occur on your account after the
> webhook is registered. If you see *"no deliveries yet"*, create or update a link (for
> `link.created` / `link.updated`) or use **Test** to generate a delivery.
---
## Verifying Webhook Signatures
Every delivery is signed with HMAC-SHA256 of the **raw request body**, using your
webhook's signing secret (shown once on creation). Always verify before trusting a payload.
`X-SID-Signature: sha256=<hex>`
### Node.js
```js
import crypto from "node:crypto";
function verify(rawBody, signatureHeader, secret) {
const expected =
"sha256=" + crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader),
);
}
```
### Go
```go
func verify(rawBody []byte, signatureHeader, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signatureHeader))
}
```
---
## OAuth 2.0
For apps acting **on behalf of other s.id users**, use the OAuth 2.0 authorization-code flow
with PKCE.
| Endpoint | Host | Description |
|----------|------|-------------|
| `GET /oauth/authorize` | `https://api.s.id` | Redirect the user's browser here; it 302s to the `dash.s.id` consent screen (user approves/denies your app + scopes) |
| `POST /oauth/token` | `https://api.s.id` | Exchange `code` for tokens; also `grant_type=refresh_token` |
| `POST /oauth/revoke` | `https://api.s.id` | Revoke an access or refresh token (RFC 7009) |
| `GET /oauth/userinfo` | `https://api.s.id` | Profile of the authorizing user (`Authorization: Bearer <access_token>`) |
> All OAuth 2.0 endpoints — including `/oauth/authorize` — are served by `api.s.id`, the same
> host as `/v2/*`. `GET /oauth/authorize` is the one your user's browser navigates to; it
> redirects (302) to the actual consent UI on the dashboard frontend at `dash.s.id`, preserving
> the query string, then the consent page calls back into `api.s.id` for the consent data it
> renders. You never need to hard-code `dash.s.id` yourself — redirect to `api.s.id` and the
> redirect chain takes care of the rest.
### PKCE (Proof Key for Code Exchange)
- **Public clients** (no client secret) MUST send `code_challenge` + `code_challenge_method=S256`
to `/oauth/authorize` — omitting it returns `code_challenge_required`.
- **Confidential clients** (issued a `client_secret`) may use PKCE too; if `code_challenge` is
present, `code_challenge_method` must still be `S256` (`plain` is rejected with
`unsupported_code_challenge_method`).
- Derive `code_challenge = BASE64URL(SHA256(code_verifier))` from a random `code_verifier`, and
send the original `code_verifier` (not the challenge) when exchanging the code at `/oauth/token`.
- An authorization `code` is **single-use**: it's consumed as soon as `/oauth/token` is called,
even if that call fails (e.g. wrong `code_verifier`). Request a fresh code via `/oauth/authorize`
to retry.
Flow:
1. Redirect the user to `https://api.s.id/oauth/authorize?client_id=...&redirect_uri=...&scope=...&response_type=code&code_challenge=...&code_challenge_method=S256`.
2. After consent, s.id redirects back to your `redirect_uri` with `?code=...&state=...`.
3. Exchange the code at `POST https://api.s.id/oauth/token` for an `access_token` + `refresh_token`
— confidential clients also send `client_secret`; send `code_verifier` if PKCE was used.
4. Call `GET https://api.s.id/oauth/userinfo` with the access token to read the authorizing
user's profile. Refresh with `grant_type=refresh_token` when it expires (this rotates the
refresh token — the old one is invalidated immediately).
> Access and refresh tokens are SHA-256 hashed at rest and have a fixed expiry (access token:
> 1 hour, refresh token: 30 days) — the raw token is returned to your app only at issue time.
> Access tokens are accepted on every `/v2/*` endpoint described above, in addition to
> `/oauth/userinfo` — send `Authorization: Bearer <access_token>` exactly as you would an
> `sk_live_*` API key. `/v2/*` enforces the same per-endpoint scopes either way, but OAuth
> grants stay granular: a token authorized for `microsites:write` will **not** implicitly gain
> `microsites:delete` the way a legacy API key does. `sk_live_*` keys remain fully supported for
> first-party server-to-server use; OAuth is for third-party integrations acting on behalf of a
> user. **Replace ≠ remove** — pick whichever fits your integration, both work on `/v2/*`.
>
> `/v2/*` and the OAuth authorization/token endpoints both live on `api.s.id` — a single host
> for every server-to-server call, whether authenticated with an `sk_live_*` key or an OAuth
> Bearer token.
>
> A `401` on `/v2/*` from an OAuth Bearer token carries a `WWW-Authenticate` header and a JSON
> body distinguishing three cases: `invalid_request` (missing/malformed header), `invalid_token`
> (unknown or revoked token — re-run the authorization flow), and `token_expired` (use the
> refresh token instead of re-authorizing).
>
> **Discovery:** `GET https://api.s.id/.well-known/oauth-authorization-server` (RFC 8414)
> advertises all endpoint URLs, `scopes_supported`, and `code_challenge_methods_supported` —
> standards-compliant OAuth libraries can configure themselves from it instead of hard-coding
> our URLs.
>
> **Token introspection:** `POST https://api.s.id/oauth/introspect` (RFC 7662) — a
> client-credential-protected endpoint for resource servers or debugging tools to check whether
> a token is alive and which scopes it carries. Body: `{"token": "...", "client_id": "...",
> "client_secret": "..."}`. Always returns `200` with `{"active": false}` for any unknown,
> expired, or revoked token (never an error), and `{"active": true, "scope": "...",
> "client_id": "...", "sub": "...", "exp": ..., "token_type": "Bearer"}` for a live one.
### OAuth errors
| Code | Meaning |
|------|---------|
| `invalid_oauth_request` | Missing `client_id`/`redirect_uri`, or `response_type` isn't `code` |
| `oauth_client_not_found` / `invalid_client` | Unknown `client_id`, or bad `client_secret` |
| `invalid_redirect_uri` | `redirect_uri` doesn't exactly match one registered for the client |
| `code_challenge_required` | Public client omitted PKCE `code_challenge` |
| `unsupported_code_challenge_method` | `code_challenge_method` other than `S256` |
| `invalid_grant` | Code/refresh token invalid, expired, already used, or `code_verifier` mismatch |
| `code_expired` | Authorization code expired (10 minute lifetime) |
| `unsupported_grant_type` | `grant_type` isn't `authorization_code` or `refresh_token` |
---
## Rate Limits & Errors
- **Rate limit:** 38 requests/minute per credential — per `sk_live_*` key or per individual OAuth
access token. Exceeding it returns `429` with `{"error":"rate_limit_exceeded"}`.
- **Per-app rate limit (OAuth only):** a second, more generous ceiling (500 requests/minute) applies
across all of an OAuth app's tokens combined, so one runaway integration with many authorized
users can't exhaust shared capacity even though no single user's token trips the per-credential
limit above. Exceeding it returns `429` with `{"error":"client_rate_limit_exceeded"}`.
- **Missing scope:** `403` with `{"error":"insufficient_scope","required":"<scope>"}`.
- **Invalid/expired key or token:** `401`. For OAuth Bearer tokens this distinguishes
`invalid_request`, `invalid_token`, and `token_expired` — see the OAuth section above.
- **Validation errors:** `422` with a descriptive message key.
Standard success envelope:
```json
{ "data": { /* ... */ }, "message": "..." }
```
---
## Legacy API (v1)
> **Deprecated — existing integrations continue to work, but new integrations should use
> the v2 API above.**
The v1 API uses a different credential system: a static **auth ID** + **auth key** pair
tied to your user account (not scoped). These credentials are issued on a per-user basis
and cannot be scoped to individual capabilities.
### Credentials
| Header | Value |
|--------|-------|
| `X-Auth-Id` | Your numeric user ID |
| `X-Auth-Key` | Your account API key |
Creating or regenerating v1 keys is **no longer available** from the dashboard. Existing
grandfathered keys remain functional. Migrate to `sk_live_*` v2 keys at your earliest
convenience.
### v1 endpoints (read-only reference)
v1 is served at `https://api.s.id/v1/` and mirrors the v2 capability set. Use the v2
paths and auth above for all new integrations.
---
> **Last updated: June 2026** — covers v2 of the s.id developer API.
> Source of truth: `docs/developer-platform-guide.md` in the s.id repo.
> Guide + OpenAPI spec propagated to both repos via `scripts/sync-developer-docs.sh`.
Tidak ada komentar:
Posting Komentar