# Skrap.ai — full documentation > Skrap.ai is a B2B email verification and email finder platform with lead and company search. It verifies email deliverability (valid, risky, invalid, unknown) with live mail-server checks, finds work emails from a name and a company or domain, searches people and companies, and runs bulk jobs. You pay only for useful results: invalid, unknown and not-found answers cost nothing. ## Pricing - Free: 100 one-time credits at sign-up, no card required. - Professional: from $19/month for 2,500 credits up to $89/month for 25,000 credits (2–10 seats). Yearly billing charges 10 months for 12; lowest rate about 0.30¢ per credit. - Enterprise: from $199/month for 75,000 credits up to $499/month for 250,000 credits (15–50 seats). Yearly billing charges 10 months for 12; lowest rate about 0.17¢ per credit. - Credits roll over while subscribed. Invalid, unknown and not-found results are always free. Details: https://skrap.ai/pricing ## Integrations - [Google Sheets™](https://skrap.ai/google-sheets) — Available. Verify lists and fill in missing work emails without leaving your sheet. - [Claude, ChatGPT & MCP](https://skrap.ai/mcp) — Available. Ask your AI assistant to verify, find and search — with your Skrap credits. - [Chrome extension](https://skrap.ai/integrations/chrome-extension) — Available. Find verified business emails from LinkedIn profiles and company websites as you browse. - [Skrap REST API](https://skrap.ai/integrations/api) — Available. Verify, find, search and run bulk jobs from your own code. - [Webhooks](https://skrap.ai/integrations/webhooks) — Available. Get a signed HTTP POST when bulk jobs finish or your credits run low. - [RapidAPI](https://skrap.ai/integrations/rapidapi) — Available. Use Skrap's email verifier and finder through your RapidAPI subscription. - [Apify](https://skrap.ai/integrations/apify) — Available. Run Skrap's email finder, verifier and lead search as Apify Actors. - [Clay](https://skrap.ai/integrations/clay) — Beta. Verify the emails in your Clay tables, then find verified work emails where they're missing. - [Zapier](https://skrap.ai/integrations/zapier) — Coming soon. Connect Skrap to 7,000+ apps: verify and find emails inside your Zaps. - [Make](https://skrap.ai/integrations/make) — Coming soon. Verify and find emails as steps in your Make scenarios. - [n8n](https://skrap.ai/integrations/n8n) — Coming soon. Self-hosted automations with Skrap verify, find and search nodes. - [HubSpot](https://skrap.ai/integrations/hubspot) — Coming soon. Send Skrap leads to HubSpot and keep contact emails verified. - [Salesforce](https://skrap.ai/integrations/salesforce) — Coming soon. Export prospects to Salesforce leads and keep their emails clean. - [Pipedrive](https://skrap.ai/integrations/pipedrive) — Coming soon. Enrich Pipedrive persons with verified work emails. - [Zoho CRM](https://skrap.ai/integrations/zoho-crm) — Coming soon. Sync enriched prospects to Zoho CRM. - [AWS Marketplace](https://skrap.ai/integrations/aws-marketplace) — Coming soon. Buy Skrap through your AWS account and pay on your AWS bill. --- # Skrap.ai API — Introduction > Verify emails, find work emails, enrich people and companies, search leads and run bulk jobs from your own code. JSON over HTTPS, one API key header, the same engine and credit rules as the web app. Base URL: `https://api.skrap.ai`. All requests and responses are JSON over HTTPS. Full reference: https://skrap.ai/docs/api.md · Webhooks: https://skrap.ai/docs/webhooks.md · OpenAPI: https://skrap.ai/openapi.json ## Quickstart 1. Create an API key in the app (Settings → API keys & MCP, paid plans): https://app.skrap.ai 2. Verify your first email: ```bash curl -X POST https://api.skrap.ai/api/v2/verify-email \ -H "x-api-key: $SKRAP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email":"jane@example.com"}' ``` ## Authentication - Create a key in the app under Settings → API keys & MCP (paid plans). Keys are shown in full only to you; store them like passwords. - Send it on every request in the x-api-key header. Keys on a team spend the team's shared credits. - Optional per-key controls: an IP allowlist (exact IPs or IPv4 ranges such as 10.0.0.0/8) and an expiry date. Requests from other IPs get 403 IP_NOT_ALLOWED; expired keys get 401 API_KEY_EXPIRED. ## Credits | Action | Cost | | --- | --- | | Verify — Valid or Risky | 1 credit | | Verify — Invalid or Unknown | Free | | Find — email found (Valid or Risky) | 1 credit | | Find — not found | Free | | Lead & company search | Free | | Email of a search result | 1 credit when found | | Reverse email lookup — person found | 2 credits | | Person enrichment — profile found (+ work email) | 1 credit (+1) | | Company enrichment — company found (+ headcount breakdown) | 1 credit (+1) | | Company contacts | 1 credit per person | | Enrichment — nothing found | Free | ## Rate limits | Plan | Limit | | --- | --- | | Free | 30 requests / minute per key | | Professional | 120 requests / minute per key | | Enterprise | 600 requests / minute per key | | Custom | Agreed with sales | Responses carry standard rate-limit headers; a 429 includes Retry-After. ## Verification statuses | Status | Meaning | | --- | --- | | `valid` | The mailbox exists and accepts mail. | | `risky` | Deliverable but uncertain — catch-all domains, full mailboxes, mail servers that would not confirm the mailbox, and best guesses in the company's email format. Send with care. | | `invalid` | The address or domain cannot receive mail. Free. | | `unknown` | We could not confirm the domain has a mail server (its DNS did not answer), or there was no address to give. Free. When retryable is true, the cause was temporary (try again later). | ## Errors Errors return JSON `{ "error": "", "code": "" }`. | HTTP | Code | Meaning | | --- | --- | --- | | 400 | `VALIDATION_ERROR` | A field is missing or malformed; the message says which. | | 401 | `INVALID_API_KEY` | Missing, unknown or revoked key. | | 401 | `API_KEY_EXPIRED` | The key passed its expiry date. | | 402 | `INSUFFICIENT_CREDITS` | Not enough credits (details.required / details.available). | | 402 | `MEMBER_CREDIT_LIMIT` | Your team admin's monthly limit for your account is used up. | | 403 | `PLAN_REQUIRED` | The feature needs a higher plan (requiredPlan in the body). | | 403 | `IP_NOT_ALLOWED` | The key's IP allowlist does not include the caller. | | 404 | `NOT_FOUND` | Unknown id (task, list, webhook). | | 429 | `RATE_LIMITED` | Your plan's per-minute limit for this key; wait for the Retry-After header. | | 429 | `RATE_LIMIT_EXCEEDED` | Too many requests at once (concurrency) or from one IP; back off and retry. | | 429 | `ENRICH_DAILY_LIMIT` | Your account's daily enrichment credit limit is used up; it resets at midnight UTC. | | 503 | `ENRICH_UNAVAILABLE` | Enrichment is busy or briefly unavailable. Nothing was charged; retry later (Retry-After). | | 5xx | `INTERNAL_ERROR` | Our side. Retry with back-off; nothing was charged. | --- # Skrap.ai API reference Base URL `https://api.skrap.ai`. Authenticate every request with the `x-api-key` header. ## Verify & find Real-time, one email or one person per call. ### Verify an email `POST /api/v2/verify-email` Checks the address against its mail server. Charged only when the result is valid or risky. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `email` | string | yes | Address to verify. | Request: ```bash curl -X POST "https://api.skrap.ai/api/v2/verify-email" \ -H "x-api-key: $SKRAP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email":"jane@example.com"}' ``` Response: ```jsonc { "email": "jane@example.com", "status": "valid", // valid | risky | invalid | unknown "retryable": false, // true: unknown for a temporary reason — try later "domainInfo": { "domain": "example.com", "hasMxRecords": true }, "score": 95, // 95 valid · 10–70 risky (by risk reason) · 0 invalid · null unknown "checks": { "syntax": true, "mxFound": true, "smtp": "accepted", // accepted | rejected | inconclusive | not_checked "catchAll": false, "disposable": false, "roleAccount": false, "freeProvider": false }, "riskReasons": [], // risky only: catch_all, catch_all_unconfirmed, role_account, disposable "didYouMean": null // typo hint, e.g. "jane@gmail.com" for "jane@gmial.com" } ``` ### Find a work email `POST /api/v2/find-email` Finds and verifies the work email of a person at a company. Give a domain when you have one (best results) or a company name. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `firstName / lastName` | string | no | Or fullName. | | `fullName` | string | no | Split into first and last name when firstName is empty. | | `domain` | string | no | Company domain, e.g. example.com. | | `company` | string | no | Company name when you have no domain. | | `location` | string | no | Optional HQ hint that helps match a company name. | Request: ```bash curl -X POST "https://api.skrap.ai/api/v2/find-email" \ -H "x-api-key: $SKRAP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"firstName":"Jane","lastName":"Doe","domain":"example.com"}' ``` Response: ```jsonc { "emails": [{ "email": "jane.doe@example.com", "status": "valid" }], "status": "valid", "domainStatus": "valid", "pendingRefinement": false, // true: poll /api/v1/find-email/refinement/{requestId} "requestId": "req_…", "retryable": false } ``` - Some mail servers answer slowly. Then the first answer has pendingRefinement: true and is not charged; poll the refinement endpoint until ready is true (charged once, when the final answer is valid or risky). - When a company name matches several companies the response asks to confirm (needsCompanyConfirmation with suggestions). Send the domain instead. ### Poll a pending find `GET /api/v1/find-email/refinement/{requestId}` Final answer of a find that returned pendingRefinement: true. Request: ```bash curl "https://api.skrap.ai/api/v1/find-email/refinement/REQUEST_ID" \ -H "x-api-key: $SKRAP_API_KEY" ``` Response: ```jsonc { "ready": true, "email": "jane.doe@example.com", "status": "valid", "domainStatus": "valid", "retryable": false } ``` ## Enrichment Turn a work email, a LinkedIn profile or a company domain into a full B2B record. Charged only when something is found; a miss is free. Built for paid plans (the free plan gets a small daily allowance). Each account can spend up to 5,000 credits a day on enrichment; contact us for more. ### Reverse email lookup `POST /api/v2/enrich/reverse-email` Who is behind a work email address: name, job title, current company, location, career and LinkedIn profile. Work (company) addresses only; personal mailbox providers such as Gmail are refused. 2 credits when a person is found (3 with includeCompany when the company profile is found). | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `email` | string | yes | Work email address. | | `includeCompany` | boolean | no | Also return the full company profile (+1 credit when found). | Request: ```bash curl -X POST "https://api.skrap.ai/api/v2/enrich/reverse-email" \ -H "x-api-key: $SKRAP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"email":"jane.doe@example.com"}' ``` Response: ```jsonc { "found": true, "email": "jane.doe@example.com", "emailMatchesCurrentJob": true, // false: the address belongs to a previous employer "person": { "fullName": "Jane Doe", "firstName": "Jane", "lastName": "Doe", "headline": "Head of Sales at Example", "linkedinUrl": "https://www.linkedin.com/in/jane-doe", "location": { "city": "Austin", "region": "TX", "country": "US" }, "currentJob": { "title": "Head of Sales", "company": { "name": "Example Inc", "domain": "example.com", "linkedinUrl": "https://www.linkedin.com/company/example" }, "startDate": "2023-04", "endDate": null, "current": true }, "experience": [ … ], // up to 15 jobs, in profile order "education": [{ "school": "State University", "degree": "BA", "startYear": 2012, "endYear": 2016 }], "skills": ["negotiation", "saas"] }, "company": { "name": "Example Inc", "domain": "example.com", "linkedinUrl": "https://www.linkedin.com/company/example" }, "creditsCharged": 2, "requestId": "req_…" } ``` - Not found: { "found": false, "person": null, "creditsCharged": 0 }. - Typical uses: qualify inbound sign-ups and form fills, complete CRM contacts that only have an email. ### Enrich a LinkedIn profile `POST /api/v2/enrich/person` Profile of a person from their LinkedIn URL, plus the work email at their current employer when we have one on record (an address at a previous employer is never returned). 1 credit when the profile is found, +1 when a work email is returned. | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `linkedinUrl` | string | yes | Profile URL, e.g. https://www.linkedin.com/in/jane-doe. | | `includeEmail` | boolean | no | Look up the work email (default true). | Request: ```bash curl -X POST "https://api.skrap.ai/api/v2/enrich/person" \ -H "x-api-key: $SKRAP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"linkedinUrl":"https://www.linkedin.com/in/jane-doe"}' ``` Response: ```jsonc { "found": true, "person": { "fullName": "Jane Doe", "currentJob": { "title": "Head of Sales", "company": { "name": "Example Inc", "domain": "example.com", … } }, … }, "workEmail": { "address": "jane.doe@example.com", "status": "valid" }, // null when none on record "creditsCharged": 2, "requestId": "req_…" } ``` ### Enrich a company `POST /api/v2/enrich/company` Company profile from a domain or a LinkedIn company URL: industry, type, size, employee count, revenue range, founded year, headquarters, followers and who hosts its email. 1 credit when found; +1 with includeBreakdown (headcount by department and by country). | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `domain` | string | no | Company domain (a website URL or an email address also works). | | `linkedinUrl` | string | no | LinkedIn company URL, when you have no domain. | | `includeBreakdown` | boolean | no | Headcount by department and by country (+1 credit). | Request: ```bash curl -X POST "https://api.skrap.ai/api/v2/enrich/company" \ -H "x-api-key: $SKRAP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain":"example.com","includeBreakdown":true}' ``` Response: ```jsonc { "found": true, "company": { "name": "Example Inc", "domain": "example.com", "website": "https://example.com", "linkedinUrl": "https://www.linkedin.com/company/example", "industry": "Software Development", "type": "Privately Held", "size": "51-200", "employees": 120, "revenueRange": "$10M-$50M", "foundedYear": 2015, "description": "Example builds software for teams.", "location": { "city": "Austin", "region": "Texas", "country": "US" }, "followers": 3000, "emailProvider": "google_workspace", // microsoft_365 | zoho | proofpoint | mimecast | barracuda | other | null "acceptsEmail": true }, "headcountBreakdown": { "total": 120, "departments": [{ "name": "Engineering", "count": 60, "percent": 50 }, … ], "countries": [{ "name": "US", "count": 100, "percent": 83.33 }, … ] }, "creditsCharged": 2, "requestId": "req_…" } ``` - Personal mailbox domains (gmail.com, outlook.com …) are not companies: found is false and nothing is charged. ### People at a company `POST /api/v2/enrich/company-contacts` People who work at a company, filtered by seniority, department and country, with their work emails when we have them on record. 1 credit per person returned (emails included). | Parameter | Type | Required | Description | | --- | --- | --- | --- | | `domain / linkedinUrl` | string | no | Company domain, or its LinkedIn company URL (one of the two is required). | | `seniority` | string[] | no | c_suite, vp, director, manager, staff, other. | | `department` | string[] | no | sales, marketing, engineering, it, finance, operations, human_resources, legal, customer_service, management, research, purchasing, supply_chain, manufacturing, healthcare, education, public_sector, science, creative, construction, writing, other. | | `country` | string[] | no | ISO 2-letter country codes, e.g. ["US", "GB"]. | | `recentHires` | boolean | no | Only people who started in the last 90 days. | | `limit` | number | no | People per page, 1–25 (default 10). Trimmed to what your balance covers. | | `page` | number | no | Page number (default 1, up to 40). | | `includeEmails` | boolean | no | Work emails (default true, no extra cost). | Request: ```bash curl -X POST "https://api.skrap.ai/api/v2/enrich/company-contacts" \ -H "x-api-key: $SKRAP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"domain":"example.com","seniority":["vp","director"],"department":["sales"],"limit":10}' ``` Response: ```jsonc { "found": true, "company": { "name": "Example Inc", "domain": "example.com", "linkedinUrl": "https://www.linkedin.com/company/example" }, "page": 1, "totalPages": 4, "people": [{ "fullName": "Jane Doe", "firstName": "Jane", "lastName": "Doe", "title": "VP Sales", "headline": "VP Sales at Example", "linkedinUrl": "https://www.linkedin.com/in/jane-doe", "location": { "city": "Austin", "region": "TX", "country": "US" }, "startDate": "2023-04", "email": { "address": "jane.doe@example.com", "status": "valid" } // null when none on record }], "creditsCharged": 1, "requestId": "req_…" } ``` ## Bulk jobs Up to 25,000 rows per job, processed in the background. Poll the task or subscribe to the bulk_job.completed webhook. ### Start a bulk verification `POST /api/v1/bulk-verify` Each row is charged once, when it settles as valid or risky. Request: ```bash curl -X POST "https://api.skrap.ai/api/v1/bulk-verify" \ -H "x-api-key: $SKRAP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"emails":["jane@example.com","john@example.org"]}' ``` Response: ```jsonc { "taskId": "65f1…", "status": "pending", "progress": { "total": 2, "processed": 0, "percentage": 0 } } ``` ### Start a bulk email find `POST /api/v1/bulk-find` Each row needs a first name and a domain or company. Request: ```bash curl -X POST "https://api.skrap.ai/api/v1/bulk-find" \ -H "x-api-key: $SKRAP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"data":[{"firstName":"Jane","lastName":"Doe","domain":"example.com"},{"firstName":"Li","company":"Acme"}]}' ``` Response: ```jsonc { "taskId": "65f1…", "status": "pending", "progress": { "total": 2, "processed": 0, "percentage": 0 } } ``` ### Job status `GET /api/v1/tasks/{taskId}` Status (pending, processing, paused, completed, failed, cancelled) and progress. Request: ```bash curl "https://api.skrap.ai/api/v1/tasks/TASK_ID" \ -H "x-api-key: $SKRAP_API_KEY" ``` Response: ```jsonc { "taskId": "65f1…", "type": "bulk-verify", "status": "completed", "progress": { "total": 2, "processed": 2, "percentage": 100 }, "controls": { … } } ``` ### Job results (paged) `GET /api/v1/tasks/{taskId}/results?offset=0&limit=50` Result rows, 50 per call by default (offset / limit). Request: ```bash curl "https://api.skrap.ai/api/v1/tasks/TASK_ID/results?offset=0&limit=50" \ -H "x-api-key: $SKRAP_API_KEY" ``` Response: ```jsonc { "taskId": "65f1…", "status": "completed", "offset": 0, "limit": 50, "total": 2, "statusCounts": { "valid": 1, "risky": 0, "invalid": 1, "unknown": 0 }, "rows": [{ "email": "jane@example.com", "status": "valid" }] } ``` ### Download results as CSV `GET /api/v1/tasks/{taskId}/download?inline=1` inline=1 streams the CSV; without it you get a short-lived signed downloadUrl. Results are kept 30 days (410 RESULTS_EXPIRED after). Request: ```bash curl "https://api.skrap.ai/api/v1/tasks/TASK_ID/download?inline=1" \ -H "x-api-key: $SKRAP_API_KEY" ``` Response: ```text email,status,… ``` ### Pause, resume or cancel `POST /api/v1/tasks/{taskId}/pause | resume | cancel` Rows already processed stay charged; nothing else is. Request: ```bash curl -X POST "https://api.skrap.ai/api/v1/tasks/TASK_ID/pause" \ -H "x-api-key: $SKRAP_API_KEY" ``` Response: ```jsonc { "message": "Task cancelled successfully", "taskId": "65f1…" } ``` ## Lists Save prospects and companies to lists and export them — the same lists you see in the app. ### Your lists `GET /api/v1/lists?kind=prospect` Names, kinds and lead counts. Request: ```bash curl "https://api.skrap.ai/api/v1/lists?kind=prospect" \ -H "x-api-key: $SKRAP_API_KEY" ``` Response: ```jsonc { "success": true, "lists": [{ "id": "65f1…", "name": "Q2 outbound", "kind": "prospect", "leadCount": 42 }] } ``` ### Create a list `POST /api/v1/lists` kind is prospect (people) or company. Request: ```bash curl -X POST "https://api.skrap.ai/api/v1/lists" \ -H "x-api-key: $SKRAP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name":"Q2 outbound","kind":"prospect"}' ``` Response: ```jsonc { "success": true, "list": { "id": "65f1…", "name": "Q2 outbound", "kind": "prospect", "leads": [] } } ``` ### Add a lead `POST /api/v1/lists/{id}/leads` id, name and company are required. Duplicate ids or emails are refused (409). Request: ```bash curl -X POST "https://api.skrap.ai/api/v1/lists/ID/leads" \ -H "x-api-key: $SKRAP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"id":"lead-123","name":"Jane Doe","company":"Example Inc","email":"jane@example.com","emailStatus":"valid","jobRole":"CTO","domain":"example.com","location":"Austin, US","linkedinUrl":"https://linkedin.com/in/…"}' ``` Response: ```jsonc { "success": true, "lead": { … } } ``` - Fires the list.lead_added webhook. ### Copy or move leads `POST /api/v1/lists/{id}/leads/transfer` To another list of the same kind; leads already in the target are skipped. Request: ```bash curl -X POST "https://api.skrap.ai/api/v1/lists/ID/leads/transfer" \ -H "x-api-key: $SKRAP_API_KEY" \ -H "Content-Type: application/json" \ -d '{"targetListId":"65f2…","leadIds":["lead-123"],"mode":"move"}' ``` Response: ```jsonc { "success": true, "mode": "move", "matched": 1, "added": 1, "skipped": 0 } ``` ### Export as CSV `GET /api/v1/lists/export?ids=all&kind=prospect` One or more lists (ids=a,b or all). Leads on your do-not-contact list are left out unless includeSuppressed=1; the X-Skrap-Suppressed header says how many. Request: ```bash curl "https://api.skrap.ai/api/v1/lists/export?ids=all&kind=prospect" \ -H "x-api-key: $SKRAP_API_KEY" ``` Response: ```text Name,Email,Email Status,Job Title,Company,Domain,Location,Industry,Headcount,LinkedIn,List ``` ## Account Who a key belongs to — handy as an integration's “test connection” call. ### Key owner and balance `GET /api/v2/account` Email, plan, spendable credits and team of the key's owner. Request: ```bash curl "https://api.skrap.ai/api/v2/account" \ -H "x-api-key: $SKRAP_API_KEY" ``` Response: ```jsonc { "id": "65f0…", "email": "you@company.com", "plan": "pro", "credits": 4210, "team": { "id": "65f3…", "name": "Sales" }, "apiKey": { "id": "65f4…", "name": "Zapier" } } ``` --- # Skrap.ai webhooks Webhooks send a signed HTTP POST to your HTTPS endpoint when something happens in Skrap. Add endpoints in the app (Integrations → Webhooks) or subscribe from an integration with the REST-hook endpoints below. Paid plans. ## Events | Event | When | | --- | --- | | `bulk_job.completed` | A bulk verify or bulk find job finished. data: jobId, type, name, total, processed, creditsUsed, statusCounts, resultsUrl. | | `bulk_job.failed` | A bulk job stopped with an error (processed rows are kept). | | `credits.low` | Your balance dropped below your alert threshold (once a day at most). data: balance, threshold, plan. | | `list.lead_added` | A lead was saved to one of your lists. data: listId, listName, lead { fullName, email, emailStatus, title, company, domain, linkedinUrl }. | ## Payload ```json { "id": "evt_65f1c2a9e4b0a1b2c3d4e5f6", "type": "bulk_job.completed", "created": "2026-11-03T09:12:44.120Z", "data": { "jobId": "65f1c2a9e4b0a1b2c3d4e5f6", "type": "bulk-verify", "total": 1200, "creditsUsed": 874, "statusCounts": { "valid": 801, "risky": 73, "invalid": 290, "unknown": 36 } } } ``` ## Headers | Header | Value | | --- | --- | | `Skrap-Event` | Event type, e.g. bulk_job.completed | | `Skrap-Event-Id` | Same for every delivery of one event — use it to de-duplicate | | `Skrap-Delivery` | Unique per delivery attempt chain | | `Skrap-Signature` | t=,v1= | ## Verify signatures ```javascript import crypto from "node:crypto"; function verifySkrapSignature(rawBody, header, secret) { const parts = Object.fromEntries(header.split(",").map((p) => p.split("="))); if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false; // replay window const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex"); return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 ?? "")); } ``` ```python import hmac, hashlib, time def verify_skrap_signature(raw_body: bytes, header: str, secret: str) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) if abs(time.time() - int(parts["t"])) > 300: return False expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get("v1", "")) ``` ## Retries Answer 2xx within 10 seconds. Anything else (or a timeout) is retried after 1 min, 5 min, 30 min, 2 h, 6 h and 12 h. Redirects are not followed. An endpoint that fails 50 times in a row is paused — re-enable it in the app. ## REST hooks (for integration builders) | Method | Path | Description | | --- | --- | --- | | POST | `/api/v2/webhooks` | Subscribe: { "url" (or "target_url"), "events": [...] (or "event") } → 201 { id, url, events, secret }. | | GET | `/api/v2/webhooks` | List subscriptions (no secrets). | | DELETE | `/api/v2/webhooks/{id}` | Unsubscribe. | | GET | `/api/v2/webhooks/samples/{event}` | Example payload (array of one) for mapping fields in an integration builder. |